All Virtuozzo development lists (kernel + QEMU)
 help / color / mirror / Atom feed
Search results ordered by [date|relevance]  view[summary|nested|Atom feed]
thread overview below | download mbox.gz: |
* [Devel] [PATCH VZ10 v5 3/9] ve/fs: Rework per-ve mount count
  @ 2026-08-02 11:40  3% ` Vladimir Riabchun
  2026-08-02 11:40  8% ` [Devel] [PATCH VZ10 v5 7/9] ve: Introduce per-VE failcount Vladimir Riabchun
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-08-02 11:40 UTC (permalink / raw)


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>
---
v4 -> v5:
 - Dropped created_as_pseudosuper, now all mounts are accounted,
   but for ve0 and pseudosuper mnt_avail_nr is allowed to go below 0.

 fs/namespace.c     | 71 +++++++++++++++++++++++++++-------------------
 include/linux/ve.h |  2 +-
 kernel/ve/ve.c     | 12 ++++----
 3 files changed, 49 insertions(+), 36 deletions(-)

diff --git a/fs/namespace.c b/fs/namespace.c
index cd6aa2127203..2550aeba2f1e 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -317,18 +317,21 @@ 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 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;
 	}
 
@@ -336,6 +339,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;
@@ -370,7 +381,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;
 
@@ -381,6 +393,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;
 }
@@ -750,7 +764,7 @@ 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);
+	ve_mount_put(mnt, mnt->ve_owner);
 	kfree_const(mnt->mnt_devname);
 #ifdef CONFIG_SMP
 	free_percpu(mnt->mnt_pcp);
@@ -3205,7 +3219,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.
 			 */
@@ -3258,30 +3272,30 @@ int ve_devmnt_process(struct ve_struct *ve, dev_t dev, void **data_pp, int remou
 	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;
 }
 
 bool is_sb_ve_accessible(struct ve_struct *ve, struct super_block *sb)
@@ -3303,9 +3317,8 @@ 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) { }
 #endif /* CONFIG_VE */
 
 static int ve_prepare_mount_options(struct fs_context *fc, void *data)
diff --git a/include/linux/ve.h b/include/linux/ve.h
index 3facbd1759df..cca0a2bc1aac 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -88,7 +88,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 dddf2393326d..3f66144eeb7e 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -81,7 +81,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),
@@ -778,7 +778,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");
@@ -1055,9 +1055,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)
@@ -1617,8 +1617,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


^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH VZ10 v5 7/9] ve: Introduce per-VE failcount
    2026-08-02 11:40  3% ` [Devel] [PATCH VZ10 v5 3/9] ve/fs: Rework per-ve mount count Vladimir Riabchun
@ 2026-08-02 11:40  8% ` Vladimir Riabchun
  2026-08-07  9:25  0%   ` Vasileios Almpanis
  2026-08-02 11:40  6% ` [Devel] [PATCH VZ10 v5 8/9] selftests/ve: Add more helpers Vladimir Riabchun
  2026-08-02 11:40  5% ` [Devel] [PATCH VZ10 v5 9/9] selftests/ve: Add mount accounting selftest Vladimir Riabchun
  3 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-08-02 11:40 UTC (permalink / raw)


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       |  6 ++++
 kernel/bpf/syscall.c     |  1 +
 kernel/ve/ve.c           | 67 ++++++++++++++++++++++++++++++++++++++++
 net/core/dev.c           |  2 ++
 net/core/neighbour.c     |  1 +
 net/core/net_namespace.c |  4 ++-
 8 files changed, 83 insertions(+), 1 deletion(-)

diff --git a/fs/aio.c b/fs/aio.c
index cb63416af135..3fa07cc626f8 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) {
+		atomic_inc(&ve->aio_failcount);
 		spin_unlock(&ve->aio_nr_lock);
 		err = -EAGAIN;
 		goto err_ctx;
diff --git a/fs/namespace.c b/fs/namespace.c
index 2550aeba2f1e..c4e2c7f7f725 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -3283,6 +3283,8 @@ static inline int ve_try_reserve_mount(struct ve_struct *ve)
 
 	if (ret)
 		get_ve(ve);
+	else
+		atomic_inc(&ve->mnt_failcount);
 	return ret;
 }
 
diff --git a/include/linux/ve.h b/include/linux/ve.h
index 5687faad46ff..9e73527e970e 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -72,12 +72,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;
@@ -86,6 +89,7 @@ struct ve_struct {
 
 	atomic_t		arp_neigh_nr;
 	atomic_t		nd_neigh_nr;
+	atomic_t		neigh_tbl_failcount;
 	unsigned long		meminfo_val;
 
 	/*
@@ -94,6 +98,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
@@ -121,6 +126,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;
 };
diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
index ff2a51c59f04..95e806fa19f4 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) {
+			atomic_inc(&load_ve->bpf_prog_failcount);
 			load_ve = NULL;
 			err = -ENOSPC;
 			goto put_token;
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index dc3faa0b1d76..a5c93be759ac 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -99,10 +99,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	=
@@ -114,8 +117,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,
 	.umh_running_helpers	= ATOMIC_INIT(0),
 	.umh_helpers_waitq	= __WAIT_QUEUE_HEAD_INITIALIZER(ve0.umh_helpers_waitq),
@@ -780,12 +791,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);
@@ -812,7 +826,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
@@ -825,6 +841,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;
@@ -1066,6 +1083,50 @@ 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));
+	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));
+	struct ve_failcount_entry *entry;
+	atomic_t *fc;
+
+	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);
@@ -1603,6 +1664,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..05e0b9b6ba23 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -10997,6 +10997,7 @@ int register_netdevice(struct net_device *dev)
 
 	ret = -ENOMEM;
 	if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
+		atomic_inc(&net->owner_ve->netif_failcount);
 		ve_pr_warn_ratelimited(VE_LOG_BOTH,
 			"CT%s: hits max number of network devices, "
 			"increase ve::netif_max_nr parameter\n",
@@ -12211,6 +12212,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) {
+		atomic_inc(&net->owner_ve->netif_failcount);
 		ve_pr_warn_ratelimited(VE_LOG_BOTH,
 			"CT%s: hits max number of network devices, "
 			"increase ve::netif_max_nr parameter\n",
diff --git a/net/core/neighbour.c b/net/core/neighbour.c
index f90deb17fb25..57a49d9c98a7 100644
--- a/net/core/neighbour.c
+++ b/net/core/neighbour.c
@@ -520,6 +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) {
+			atomic_inc(&ve->neigh_tbl_failcount);
 			net_info_ratelimited("%s: neighbor table overflow!\n",
 					     tbl->id);
 			NEIGH_CACHE_STAT_INC(tbl, table_fulls);
diff --git a/net/core/net_namespace.c b/net/core/net_namespace.c
index b3d54cad984a..9a3376d2682f 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) {
+		atomic_inc(&ve->netns_failcount);
 		return -ENOSPC;
+	}
 	return 0;
 }
 
-- 
2.47.1


^ permalink raw reply	[relevance 8%]

* [Devel] [PATCH VZ10 v5 8/9] selftests/ve: Add more helpers
    2026-08-02 11:40  3% ` [Devel] [PATCH VZ10 v5 3/9] ve/fs: Rework per-ve mount count Vladimir Riabchun
  2026-08-02 11:40  8% ` [Devel] [PATCH VZ10 v5 7/9] ve: Introduce per-VE failcount Vladimir Riabchun
@ 2026-08-02 11:40  6% ` Vladimir Riabchun
  2026-08-02 11:40  5% ` [Devel] [PATCH VZ10 v5 9/9] selftests/ve: Add mount accounting selftest Vladimir Riabchun
  3 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-08-02 11:40 UTC (permalink / raw)


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-135520

Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
v4 -> v5:
 - In run_in_ve helper CLONE_NEWVE flag is added to unshare_flags
   unconditionally.

 tools/testing/selftests/ve/ve_selftest.h | 81 ++++++++++++++++++++++--
 1 file changed, 75 insertions(+), 6 deletions(-)

diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index 69c0a52dd7ef..48bb7d1871bd 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -43,6 +43,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[20];
+
+	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)
 {
@@ -73,19 +81,31 @@ static inline int read_u64_at(int dirfd, const char *path,
 			      unsigned long long *out)
 {
 	char buf[32] = {0}, *end;
-	int fd, ret;
+	int ret;
 
-	fd = openat(dirfd, path, O_RDONLY);
-	if (fd < 0)
+	ret = read_file_at(dirfd, path, buf, sizeof(buf));
+	if (ret <= 0)
 		return -1;
 
-	ret = read(fd, buf, sizeof(buf) - 1);
-	close(fd);
+	errno = 0;
+	*out = strtoull(buf, &end, 10);
+	if (errno || end == buf)
+		return -1;
+	return 0;
+}
+
+static inline int read_s32_at(int dirfd, const char *path,
+			      int *out)
+{
+	char buf[32] = {0}, *end;
+	int ret;
+
+	ret = read_file_at(dirfd, path, buf, sizeof(buf));
 	if (ret <= 0)
 		return -1;
 
 	errno = 0;
-	*out = strtoull(buf, &end, 10);
+	*out = strtol(buf, &end, 10);
 	if (errno || end == buf)
 		return -1;
 	return 0;
@@ -134,6 +154,55 @@ static inline int enter_cgroup(int cgv2_fd, int ctid)
 	return ret;
 }
 
+/*
+ * Run function in VE cgroup and new namespaces.
+ *
+ * Namespaces are provided via unshare_flags.
+ * CLONE_NEWVE flag is set by this function.
+ * Return values:
+ *  -  0 if function returns zero
+ *  - -1 if function returns negative value
+ *  -  1 if setup fails or function returns positive value
+ */
+static inline int run_in_ve(int cgv2_fd, int ctid, int unshare_flags,
+		int (*fn)(void *), void *arg)
+{
+	int status;
+	pid_t pid;
+
+	unshare_flags |= CLONE_NEWVE;
+	pid = fork();
+	if (pid < 0) {
+		fprintf(stderr, "%s: fork failed\n", __func__);
+		return 1;
+	}
+	if (pid == 0) {
+		int ret;
+
+		if (enter_cgroup(cgv2_fd, ctid) < 0) {
+			fprintf(stderr, "%s: enter_cgroup failed\n", __func__);
+			_exit(255);
+		}
+		if (unshare(unshare_flags) < 0) {
+			fprintf(stderr, "%s: unshare(%d) failed\n",
+				__func__, unshare_flags);
+			_exit(255);
+		}
+		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) == 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.47.1


^ permalink raw reply	[relevance 6%]

* [Devel] [PATCH VZ10 v5 9/9] selftests/ve: Add mount accounting selftest
                     ` (2 preceding siblings ...)
  2026-08-02 11:40  6% ` [Devel] [PATCH VZ10 v5 8/9] selftests/ve: Add more helpers Vladimir Riabchun
@ 2026-08-02 11:40  5% ` Vladimir Riabchun
  2026-08-07 10:01  0%   ` Vasileios Almpanis
  3 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-08-02 11:40 UTC (permalink / raw)


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. Patial 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 accoutning.
6. Failcount feature verification.

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

Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
v4 -> v5:
 - Splitted pseudosuper test into two.
 - Added new test to check that things go smoothly when
   we run out of mounts in the middle of a new mount.

 tools/testing/selftests/ve/.gitignore         |   1 +
 tools/testing/selftests/ve/Makefile           |   1 +
 .../selftests/ve/ve_mount_accounting_test.c   | 419 ++++++++++++++++++
 3 files changed, 421 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..b295290ec6e8
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_mount_accounting_test.c
@@ -0,0 +1,419 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ve_mount_accounting selftests
+ *
+ * Tests to check the correctness of mount accounting.
+ */
+#define _GNU_SOURCE
+#include <linux/sched.h>
+#include <linux/mount.h>
+#include <sched.h>
+#include <sys/wait.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+#include <asm/unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/mount.h>
+#include <linux/limits.h>
+#include <errno.h>
+
+#include "../kselftest_harness.h"
+#include "ve_selftest.h"
+
+#define TMP_DIR			"/ve-mnt-tmp/"
+#define VE_MOUNTS_MAX		128
+
+static int set_pseudosuper(int cgv2_fd, int ctid, int value)
+{
+	char path[64];
+
+	snprintf(path, sizeof(path), "%d/ve.pseudosuper", ctid);
+	return write_u64_at(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 ret;
+}
+
+static int create_mount(int cgv2_fd, int ctid, int id)
+{
+	int ret = run_in_ve(cgv2_fd, ctid, CLONE_NEWVE, _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(int cgv2_fd, int ctid, int id)
+{
+	int ret = run_in_ve(cgv2_fd, ctid, CLONE_NEWVE, _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(int cgv2_fd, int ctid)
+{
+	int avail1, avail2, mnt_id;
+	char path[64];
+
+	mnt_id = get_free_mnt_id();
+
+	snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", ctid);
+	if (mnt_id < 0 ||
+	    read_s32_at(cgv2_fd, path, &avail1) ||
+	    create_mount(cgv2_fd, ctid, mnt_id) ||
+	    read_s32_at(cgv2_fd, path, &avail2) ||
+	    destroy_mount(cgv2_fd, ctid, mnt_id))
+		return -1;
+
+	return avail1 - avail2;
+}
+
+/* Expect mount success and return new avail value */
+static int mount_and_get_avail(struct __test_metadata *_metadata,
+			int cgv2_fd, int ctid, int mnt_id)
+{
+	char path_avail[64];
+	int mnt_avail_nr;
+
+	snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", ctid);
+
+	ASSERT_EQ(create_mount(cgv2_fd, ctid, mnt_id), 0);
+	ASSERT_EQ(read_s32_at(cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	return mnt_avail_nr;
+}
+
+/* Expect mount failure and ensure intact avail number */
+static void assert_mount_fails(struct __test_metadata *_metadata,
+			int cgv2_fd, int ctid, 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", ctid);
+
+	ASSERT_EQ(read_s32_at(cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, avail_count);
+	ASSERT_LT(create_mount(cgv2_fd, ctid, mnt_id), 0);
+	ASSERT_EQ(read_s32_at(cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, avail_count);
+}
+
+FIXTURE(ve_mnt_acc)
+{
+	int cgv2_fd;
+	int ctid;
+};
+
+FIXTURE_SETUP(ve_mnt_acc)
+{
+	unsigned long long 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_u64_at(self->cgv2_fd, path, &initial_mnt_avail_nr), 0);
+	ASSERT_EQ(initial_mnt_avail_nr, VE_MOUNTS_MAX);
+};
+
+FIXTURE_TEARDOWN(ve_mnt_acc)
+{
+	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->cgv2_fd, self->ctid, 0),
+		  original_mnt_avail);
+
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 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 original_mnt_avail, mnt_avail_nr, mnt_cost;
+	int original_have_mnt;
+	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);
+
+	mnt_cost = get_mount_cost(self->cgv2_fd, self->ctid);
+	ASSERT_GE(mnt_cost, 1);
+
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &original_mnt_avail), 0);
+	original_have_mnt = VE_MOUNTS_MAX - original_mnt_avail;
+
+	/* Step 1: reduce number of available mounts to mnt_cost */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, original_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 2: do one mount, no mounts should be available */
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 0),
+		  0);
+
+	/* Step 3: check that one more mount falils */
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 1, 0);
+
+	/* Step 4: increase mount limit a little bit, mount should still fail */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+				original_have_mnt + 2 * mnt_cost - 1), 0);
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 1, mnt_cost - 1);
+
+	/* Step 5: increase by 1 and win now */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, original_have_mnt + 2 * mnt_cost), 0);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, mnt_cost);
+
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 1),
+		  0);
+
+	/* Step 6: reduce mnt_max_nr so we have more mounts than allowed */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, original_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 7: try to do mount when avail < 0, ensure number is intact */
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, -1 * mnt_cost);
+
+	/* Step 8: remove one mount, check avail value update, mount should fail */
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 0), 0);
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, 0);
+
+	/* Step 9: remove one more mount and check that new mount succeeds */
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 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(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 2),
+		  0);
+
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 2), 0);
+}
+
+/*
+ * Mount propagation makes one mount cost more.
+ * This test check that if we run out or 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 mount_cost, i, orig_have, orig_mnt_avail;
+
+	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);
+
+	mount_cost = get_mount_cost(self->cgv2_fd, self->ctid);
+	ASSERT_GE(mount_cost, 1);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &orig_mnt_avail), 0);
+	orig_have = VE_MOUNTS_MAX - orig_mnt_avail;
+
+	if (mount_cost == 1)
+		SKIP(return, "mount cost is 1, no partial mounts possible");
+
+	for (i = 0; i < mount_cost; i++) {
+		ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, orig_have + i), 0);
+		assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 0, i);
+	}
+}
+
+/* Test that pseudosuper allows negative avail with correct accounting. */
+TEST_F(ve_mnt_acc, pseudosuper_allows_overuse)
+{
+	int orig_mnt_avail, orig_have;
+	int mount_cost;
+	char path_avail[64], path_max_nr[64];
+
+	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);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &orig_mnt_avail), 0);
+	orig_have = VE_MOUNTS_MAX - orig_mnt_avail;
+
+	mount_cost = get_mount_cost(self->cgv2_fd, self->ctid);
+	ASSERT_GE(mount_cost, 1);
+
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, orig_have + mount_cost), 0);
+	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 1), 0);
+
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 0),
+		  0);
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 1),
+		  -1 * mount_cost);
+
+	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 0), 0);
+
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, -1 * mount_cost);
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 0), 0);
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, 0);
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 1), 0);
+	ASSERT_EQ(get_mount_cost(self->cgv2_fd, self->ctid), mount_cost);
+}
+
+/* Test that pseudosuper doesn't disable accounting. */
+TEST_F(ve_mnt_acc, pseudosuper_continues_accounting)
+{
+	int orig_mnt_avail, mount_cost, 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);
+	mount_cost = get_mount_cost(self->cgv2_fd, self->ctid);
+	ASSERT_GE(mount_cost, 1);
+
+	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 0), 0);
+
+	/* mnt 0 - mounted without pseudosuper, umounted with it. */
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 0),
+		  orig_mnt_avail - mount_cost);
+	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 1), 0);
+
+	/* Cost is the same when mount/umount happen under pseudosuper. */
+	ASSERT_EQ(get_mount_cost(self->cgv2_fd, self->ctid), mount_cost);
+
+	/* mnt 1 - mounted with pseudosuper, umounted without it. */
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 1),
+		  orig_mnt_avail - 2 * mount_cost);
+
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 0), 0);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, orig_mnt_avail - mount_cost);
+
+	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 0), 0);
+
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 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->cgv2_fd, self->ctid, 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->cgv2_fd, self->ctid, 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->cgv2_fd, self->ctid, 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->cgv2_fd, self->ctid, 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);
+}
+
+TEST_HARNESS_MAIN
-- 
2.47.1


^ permalink raw reply	[relevance 5%]

* Re: [Devel] [PATCH vz10 v2] ve/vtty: fix use-after-free on concurrent tty close and reopen
       [not found]     <20260630160639.3043321-1-eva.kurchatova@virtuozzo.com>
@ 2026-08-04 11:15  7% ` Vasileios Almpanis
  0 siblings, 0 replies; 200+ results
From: Vasileios Almpanis @ 2026-08-04 11:15 UTC (permalink / raw)



On 6/30/26 6:06 PM, Eva Kurchatova wrote:
> Two races in the vtty subsystem lead to use-after-free of tty_struct
> objects when vzctl console attach/detach cycles run concurrently with
> container-side tty open/close (e.g. SAK-triggered getty respawn):
>
> Race 1: double-final between concurrent vtty master and slave close.
>
> In tty_release(), the vttys (slave) side has o_tty == NULL because its
> driver subtype is PTY_TYPE_SLAVE, so the final-close check depends
> solely on !slave->count.  When master and slave close concurrently,
> both sides can independently determine final == true: the slave sees
> slave->count == 0, the master sees both counts == 0 (after the slave
> count underflows to -1 and gets reset).  Both then call
> tty_release_struct -> release_tty, and the second caller hits a
> use-after-free.
>
> Fix this by adding a vtty-specific check after computing final: for
> vttys closes, also verify that the peer vttym count is not positive.
> This is safe without holding the vttym lock because the vttym side of
> tty_release holds tty_lock_slave(vttys) while decrementing vttym->count,
> so while we hold tty_lock(vttys) the vttym cannot have decremented yet.
>
> Race 2: vtty_open_master reopens a dying tty pair.
>
> After tty_release() sets final == true and releases tty_lock, there is
> a window before release_tty() runs under tty_mutex.  During this window
> vtty_open_master() can find the old vttym in the vtty map with count == 0,
> pass the ">= 1" check (which was designed as a "one vttym at a time"
> guard, not a liveness check), re-increment the counts, and hand out a
> file descriptor pointing to a tty_struct that is about to be freed.
>
> Fix this by taking tty_lock(vttys) in vtty_open_master() to serialize
> with a concurrent tty_release() on the vttys side, so we read the slave
> count after any in-flight close has decremented it.  If vttys->count
> has reached zero the pair is dying, so return -EBUSY.  Incrementing
> vttys->count under the same tty_lock prevents a concurrent tty_release()
> from seeing zero and entering the final-close path.
>
> The lock nesting pattern of tty_mutex -> tty_lock is preserved, which
> prevents any kind of A->B, B->A circular locking dependency.
>
> Fixes: dfe187803cf9 ("ve/vtty: Don't close unread master peer if slave is nonzero")
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
> ---
>   drivers/tty/pty.c    | 31 ++++++++++++++++++++++++++-----
>   drivers/tty/tty_io.c | 17 +++++++++++++++++
>   include/linux/ve.h   |  1 +
>   3 files changed, 44 insertions(+), 5 deletions(-)
>
> diff --git a/drivers/tty/pty.c b/drivers/tty/pty.c
> index f8610c77817a..2336204265ab 100644
> --- a/drivers/tty/pty.c
> +++ b/drivers/tty/pty.c
> @@ -628,6 +628,11 @@ bool vtty_is_master(struct tty_struct *tty)
>   	return tty->driver == vttym_driver;
>   }
>   
> +bool vtty_is_slave(struct tty_struct *tty)
> +{
> +	return tty->driver == vttys_driver;
> +}
> +
>   typedef struct {
>   	envid_t			veid;
>   	struct tty_struct	*vttys[MAX_NR_VTTY_CONSOLES];
> @@ -1082,15 +1087,31 @@ int vtty_open_master(envid_t veid, int idx)
>   		goto err_install;
>   	}
>   
> -	vtty_drop_context();
> -
>   	/*
> -	 * We're the master peer so increment
> -	 * slave counter as well.
> +	 * Serialize with a concurrent tty_release() on the vttys side.
> +	 * tty_lock guarantees we see the slave count after any in-flight
> +	 * close has decremented it.  If the slave has reached zero the
> +	 * pair is dying; return -EBUSY and let the caller retry; the
> +	 * old pair will be freed once we drop tty_mutex below, and the
> +	 * next attempt will create a fresh one.
> +	 *
> +	 * Incrementing slave->count under the same tty_lock prevents a
> +	 * concurrent tty_release() from seeing zero and starting the
> +	 * final-close path that would cause use-after-free.
>   	 */
> +	tty_lock(tty->link);
This check breaks attach to a console with no container-size opener. For 
fresh tty above you, decremented count to 0 and because of that opening 
with break with EBUSY.
> +	if (tty->link->count == 0) {
> +		tty_unlock(tty->link);
> +		ret = -EBUSY;
> +		goto err_install;
> +	}
> +	tty->link->count++;
> +	tty_unlock(tty->link);
> +
> +	vtty_drop_context();
> +
>   	tty_add_file(tty, file);
>   	tty->count++;
> -	tty->link->count++;
>   	fd_install(fd, file);
>   	vtty_open(tty, file);
>   
> diff --git a/drivers/tty/tty_io.c b/drivers/tty/tty_io.c
> index 797a9d0ddd1e..507da79a24fa 100644
> --- a/drivers/tty/tty_io.c
> +++ b/drivers/tty/tty_io.c
> @@ -1864,6 +1864,23 @@ int tty_release(struct inode *inode, struct file *filp)
>   	/* check whether both sides are closing ... */
>   	final = !tty->count && !(o_tty && o_tty->count);
>   
> +#ifdef CONFIG_VE
> +	/*
> +	 * vtty: prevent double-final between concurrent master and slave close.
> +	 *
> +	 * For vtty slaves o_tty is NULL (PTY_TYPE_SLAVE), so the standard final
> +	 * check only looks at slave->count.
> +	 *
> +	 * When the master is closing concurrently, it holds tty_lock_slave(slave)
> +	 * while decrementing master->count, so while we hold tty_lock(slave),
> +	 * the master cannot have decremented yet - master->count is still > 0.
> +	 * If master->count is already 0 here, the master already returned
> +	 * from tty_release with final = false, therefore we are closing it.
> +	 */
Can this really be reached now and be true? Since the ref increment and 
decrement happens under the same slave lock, can there be a case that 
slave->count == 0 and master->count > 0 were final was true? After 
adding locking in vtty_open_master this looks kinda impossible to me.

1) Slave closes first, master second:
 ? ?slave:? S 2->1, final = 0, check not reached
 ? ?master: S 1->0, M 1->0, final = 1, master frees the pair

2) Master closes first, slave second:
 ? ?master: S 2->1, M 1->0, final = 0
 ? ?slave:? S 1->0, final = 1, check reached: link->count == 0,
 ? ? ? ? ? ?condition false, slave frees the pair

3) Concurrent close, master's block takes tty_lock(slave) first:
 ? ?same as 2.

4) Concurrent close, slave's block takes the lock first:
 ? ?same as 1.

5) Several slave fds (M=1, S=N+1):
 ? ?slave closes can only bring S down to 1 while the master ref
 ? ?is present; S reaches 0 either inside the master's block
 ? ?(master frees) or in a slave's block after the master already
 ? ?closed (M == 0, condition false). Fallsback to cases 1-4.

6) No master attached (M=0, S=nfds):
 ? ?last slave close: S 1->0, final = 1, link->count == 0,
 ? ?condition false, slave frees the pair.

7) Master attached, no slave fd ever opened (M=1, S=1):
 ? ?detach: S 1->0, M 1->0, final = 1, master frees the pair.
 ? ?No slave release ever runs, check never executes.

8) Slave close racing vtty_open_master():
 ? ?attach order is: lock slave, check S != 0, S++, unlock, then
 ? ?M++. If the attach's locked section runs first, the closing
 ? ?slave sees S 2->1 and final = 0. If the close's block runs
 ? ?first, the attach cannot have done M++ yet (it comes strictly
 ? ?after its locked section, which is still waiting on the lock
 ? ?the closer holds), so the check reads M == 0, the slave frees
 ? ?the pair, and the attach then sees S == 0 and takes the
 ? ?dying-pair exit.

FYI: I asked AI to format these cases so they are more readable.


> +	if (final && !o_tty && tty->link && vtty_is_slave(tty) && tty->link->count > 0)
> +		final = 0;
> +#endif
> +
>   	tty_unlock_slave(o_tty);
>   	tty_unlock(tty);
>   
> diff --git a/include/linux/ve.h b/include/linux/ve.h
> index b037f60225bb..bf5d9acb964c 100644
> --- a/include/linux/ve.h
> +++ b/include/linux/ve.h
> @@ -236,6 +236,7 @@ extern int vtty_open_master(envid_t veid, int idx);
>   extern void vtty_release(struct tty_struct *tty, struct tty_struct *o_tty,
>   			int *tty_closing, int *o_tty_closing);
>   extern bool vtty_is_master(struct tty_struct *tty);
> +extern bool vtty_is_slave(struct tty_struct *tty);
>   extern void vtty_alloc_tty_struct(const struct tty_driver *driver,
>   				  struct tty_struct *o_tty);
>   #endif /* CONFIG_TTY */

-- 
Best regards, Vasileios Almpanis
Software Developer, Virtuozzo.


^ permalink raw reply	[relevance 7%]

* [Devel] [PATCH vz10 v8 1/1] fs: enforce container device-mount policy in the common mount path
@ 2026-08-04 12:53  3% Vasileios Almpanis
  2026-08-05 16:41  0% ` Pavel Tikhomirov
  2026-08-06 15:49  3% ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
  0 siblings, 2 replies; 200+ results
From: Vasileios Almpanis @ 2026-08-04 12:53 UTC (permalink / raw)


In a container the per-device ve_devmnt policy restricts which options
a device may be mounted with and force-inserts a set of hidden options.
The check used to run inside the option-string parser
(vfs_parse_monolithic_sep) and, for remount, in a separate helper. Two
things escaped it:

  - MS_* flags from the legacy mount(2)/fsconfig(2) API are folded into
    fc->sb_flags and never appear in the option string, so a container
    could set MS_RDONLY, MS_SYNCHRONOUS, MS_MANDLOCK, ... outside its
    allowed set.

  - The check sat in filesystem-selectable callbacks (->parse_monolithic,
    ->mount), so a filesystem not routing through them evaded the policy,
    and a skipped hidden-option insertion dropped a container's mandated
    options without error.

Enforce the policy in the fs-agnostic common mount path instead:
vfs_get_tree() for a new mount and reconfigure_super() for a remount.
The device is taken from the mounted superblock, so fc->source cannot be
raced to target another device, and fc->sb_flags is vetted alongside the
option string. On a new mount the forced options must also be present,
so a filesystem that skipped inserting them has its mount refused rather
than silently losing them.

The parse-time ve_devmnt_process() call is kept as a best-effort early
reject, so a disallowed device or option is refused before the
filesystem's fill_super() runs.

Fix a bug where a containers that use the legacy mount(2) syscall are able
to reconfigure a mount and change the superblock flags, for example
from RO to RW. Compute the effective superblock flags and emit rw incase
SB_RDONLY is missing in vfs_format_sb_flags.

Explicitly deny mounts for device backed legacy filesystems. So even
if FS_VIRTUALISED is added to them, mounting will not succeed since
legacy filesystems don't go through vfs_parse_monolithic_sep and the
policy is not applied to them. Keep this check until all filesystems
have migrated to the new mount API.

https://virtuozzo.atlassian.net/browse/VSTOR-132330
Fixes: 263467c864c5 ("ve/fs/devmnt: process mount options")
Signed-off-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
Co-developed-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
Signed-off-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>

Feature: ve: ve generic structures
---

Changes since v7:
- Add ve_devmnt_deny_legacy called by ve_devmnt_verify_fc only for
  mounts that required a block device and for non super VEs. With a
  comment explaining the rationale behind it, and for it to be removed
  in the future when all filesystem types have migrated to the new mount
  API namely when our kernels base becomes newer than v6.15. This will
  deny any mount in containers that are trying to use legacy filesystem
  types like ext2 even if some developer adds the FS_VIRTUALISED flag
  and recomplies the kernel with a warning.

Changes since v6:
 - Enforce sb-flag changes on remount: emit "rw" when SB_RDONLY is cleared
   and vet the effective flags,
   (sb->s_flags & ~fc->sb_flags_mask) | (fc->sb_flags & fc->sb_flags_mask),
   so a legacy mount(2) remount can no longer flip RO->RW unchecked.
 - Constrain only devices listed in ve.mount_opts: an unlisted device
   mounted with no userspace options is allowed rather than refused by the
   synthesized ro/rw token (tracked via have_user_opts).
 - Reset fc->ve_final_opts in vfs_dup_fs_context() to avoid a double-free.
 - Factor per-option emit into __vfs_emit_flag(); checkpatch/style fixes.

Changes since v5:
 - Fix use-after-free: do_new_mount() freed the merged page that
   legacy_get_tree()->mount() dereferences later; the fs now sees only
   the caller-owned original data.
 - Fix remount,dirsync failing with -EINVAL in containers: stop re-parsing
   the formatted flag names (SB_DIRSYNC is not in MS_RMT_MASK).
 - Check policy only: legacy fs parsers no longer get synthesized ro/sync
   tokens, and the flag check reads fc->sb_flags symmetrically for mount
   and fsconfig.
 - Keep ve_check_mount_options() only on the remount path (pinned sb
   device, covers legacy ->reconfigure); drop legacy_merge_mount_data()
   and ve_prepare_mount_options(); add Fixes: tag and rewrite the message.

Changes since v4:
 - Emit only the positive sb-flag names, not the clear names (rw/async/
   ...); on legacy remount sb_flags_mask is MS_RMT_MASK, so the clear
   names had wrongly rejected ordinary in-container remounts.
 - NUL-terminate the options page when data is empty and no flags emitted.
 - Fix __vfs_format_flags() comment (-E2BIG, not -ENOSPC).

Changes since v3:
 - Drop excess length check in legacy_merge_mount_data().

Changes since v2:
 - Remove the legacy_merge_mount_data guard in fs/internal.h.
 - Add __vfs_format_flags() helper, used by vfs_format_sb_flags().
 - Use -E2BIG (not -ENOSPC) for the buffer-too-small case.

Changes since v1:
 - Unify the comma-insert-copy pattern across call sites via an
   append_entry() helper.
 - Rework legacy_merge_mount_data() to allocate the page upfront and
   append sb flags via vfs_format_sb_flags(), dropping flags_buf[128]
   and the size arithmetic.
 - Use -ENOSPC (not -EINVAL) for buffer-too-small; comment
   FS_BINARY_MOUNTDATA; minor blank-line cleanups.

 fs/fs_context.c            | 193 +++++++++++++++++++++++++++++++++++++
 fs/internal.h              |   8 ++
 fs/namespace.c             | 110 ++++++++++++++++-----
 fs/super.c                 |  12 +++
 include/linux/fs_context.h |   2 +
 include/linux/mount.h      |   2 +
 6 files changed, 304 insertions(+), 23 deletions(-)

diff --git a/fs/fs_context.c b/fs/fs_context.c
index 76f34f3d468e..b2bd21a42083 100644
--- a/fs/fs_context.c
+++ b/fs/fs_context.c
@@ -81,6 +81,70 @@ static int vfs_parse_sb_flag(struct fs_context *fc, const char *key)
 	return -ENOPARAM;
 }
 
+/*
+ * Emit option @name into @buff at *@off, prefixed with ',' if the buffer
+ * already holds text. Advances *@off. Returns 0 or -E2BIG if @buff is full.
+ */
+static int __vfs_emit_flag(const char *name, char *buff,
+			   size_t size, size_t *off)
+{
+	ssize_t ret;
+
+	if (*off) {
+		if (*off + 1 >= size)
+			return -E2BIG;
+		buff[(*off)++] = ',';
+	}
+
+	ret = strscpy(buff + *off, name, size - *off);
+	if (ret < 0)
+		return -E2BIG;
+	*off += ret;
+	return 0;
+}
+
+static int __vfs_format_flags(const struct constant_table *p, unsigned int flags,
+			      char *buff, size_t size, size_t *off)
+{
+	for (; p->name; p++) {
+		int ret;
+
+		if (!(flags & p->value))
+			continue;
+		ret = __vfs_emit_flag(p->name, buff, size, off);
+		if (ret)
+			return ret;
+	}
+	return 0;
+}
+
+static int vfs_format_sb_flags(char *buff, size_t size, size_t *off,
+			       unsigned int sb_flags)
+{
+	int err;
+
+	err = __vfs_format_flags(common_set_sb_flag, sb_flags, buff, size, off);
+	if (err)
+		return err;
+
+	/*
+	 * "rw" has no flag bit of its own - it is simply the absence of
+	 * SB_RDONLY. Emit it explicitly so the ve_devmnt policy can allow or
+	 * deny read-write access as a first-class option; otherwise a mount or
+	 * remount that leaves the superblock read-write carries no token and
+	 * slips past the "every option must be allowed" check.
+	 *
+	 * @sb_flags is the effective post-operation flag word, so this reflects
+	 * the state the superblock actually ends up in. A remount that only
+	 * touches an unrelated flag (e.g. "sync") keeps its current SB_RDONLY
+	 * and so does not emit "rw".
+	 */
+	if (!(sb_flags & SB_RDONLY))
+		return __vfs_emit_flag("rw", buff, size, off);
+
+	return 0;
+}
+
 /**
  * vfs_parse_fs_param_source - Handle setting "source" via parameter
  * @fc: The filesystem context to modify
@@ -224,6 +288,119 @@ static inline int fscontext_lookup_bdev(struct fs_context *fc, dev_t *s_dev)
 	return -ENODEV;
 }
 
+/*
+ * ve_devmnt_deny_legacy - refuse a device mount that escapes the policy
+ * @fc: the mount context
+ *
+ * A legacy context, one for a filesystem with no ->init_fs_context passes
+ * its mount data straight to ->mount()/->remount_fs() through
+ * legacy_parse_monolithic(), so vfs_parse_monolithic_sep() never runs on it.
+ * The container's forced options are then never inserted and fc->ve_final_opts
+ * stays empty, which would leave ve_devmnt_verify_fc() vetting an empty option
+ * string while the filesystem acts on the full one userspace passed.
+ *
+ * Returns 0 if the mount may go ahead, -EPERM if it must not.
+ */
+static int ve_devmnt_deny_legacy(struct fs_context *fc)
+{
+	struct ve_struct *ve = get_exec_env();
+
+	if (fc->ops != &legacy_fs_context_ops)
+		return 0;
+
+	ve_pr_warn_ratelimited(VE_LOG_BOTH,
+			       "VE%s: refusing to mount %s: filesystem has no fs_context support\n",
+			       ve_name(ve), fc->fs_type->name);
+	return -EPERM;
+}
+
+/*
+ * ve_devmnt_verify_fc - check a mount against the container device-mount policy
+ * @fc: the mount context, with fc->root set
+ * @new_mount: true at vfs_get_tree() (new mount), false at reconfigure_super()
+ *
+ * Vets the stashed userspace option string plus the synthesized SB_* flag
+ * names against the mounted superblock's device. A device absent from the
+ * policy that is mounted with no userspace options is allowed. Returns 0 when
+ * permitted (or no check applies), or a negative errno.
+ */
+int ve_devmnt_verify_fc(struct fs_context *fc, bool new_mount)
+{
+	struct ve_struct *ve = get_exec_env();
+	unsigned int sb_flags;
+	bool have_user_opts;
+	size_t off = 0;
+	char *page;
+	int err;
+
+	if (ve_is_super(ve))
+		return 0;
+
+	if (!fc->fs_type || !(fc->fs_type->fs_flags & FS_REQUIRES_DEV))
+		return 0;
+
+	/*
+	 * Keep this check until all filesystems have migrated to the new
+	 * mount API
+	 */
+	err = ve_devmnt_deny_legacy(fc);
+	if (err)
+		return err;
+
+	/*
+	 * Filesystems with binary mount data (e.g. btrfs) bypass option
+	 * string parsing entirely, so our checks cannot apply here.
+	 */
+	if (fc->fs_type->fs_flags & FS_BINARY_MOUNTDATA)
+		return 0;
+
+	if (WARN_ON_ONCE(!fc->root))
+		return -EINVAL;
+
+	page = (char *)__get_free_page(GFP_KERNEL);
+	if (!page)
+		return -ENOMEM;
+
+	/*
+	 * Track whether userspace actually supplied options. @page below also
+	 * gets the synthesized ro/rw flag token, so its length cannot answer
+	 * this; ve_final_opts holds only the userspace string.
+	 */
+	have_user_opts = fc->ve_final_opts && *fc->ve_final_opts;
+	if (have_user_opts) {
+		ssize_t ret = strscpy(page, fc->ve_final_opts, PAGE_SIZE);
+
+		if (ret < 0) {
+			err = -E2BIG;
+			goto out;
+		}
+		off = ret;
+	}
+
+	/*
+	 * On a remount fc->sb_flags holds only the bits being changed, so
+	 * combine them with the current superblock flags to get the state the
+	 * sb will actually have - the same value reconfigure_super() writes
+	 * back. On a new mount fc->sb_flags is already the full flag word.
+	 */
+	sb_flags = fc->sb_flags;
+	if (!new_mount)
+		sb_flags = (fc->root->d_sb->s_flags & ~fc->sb_flags_mask) |
+			   (fc->sb_flags & fc->sb_flags_mask);
+
+	err = vfs_format_sb_flags(page, PAGE_SIZE, &off, sb_flags);
+	if (err)
+		goto out;
+
+	page[off] = '\0';
+	err = ve_devmnt_verify(ve, fc->root->d_sb->s_dev, page, new_mount,
+			       have_user_opts);
+
+out:
+	free_page((unsigned long)page);
+	return err;
+}
+
 static int fscontext_init_lazy_opts(struct fs_context *fc)
 {
 	struct ve_struct *ve = get_exec_env();
@@ -389,10 +566,22 @@ int vfs_parse_monolithic_sep(struct fs_context *fc, void *data,
 			return -ENODEV;
 		}
 
+		/* Early reject and hidden-option insertion; verified for real later. */
 		ret = ve_devmnt_process(ve, bd_dev, (void **) &options,
 				fc->purpose == FS_CONTEXT_FOR_RECONFIGURE);
 		if (ret)
 			return ret;
+
+		/* Stash what the filesystem parses; checked in the common mount path. */
+		if (options) {
+			kfree(fc->ve_final_opts);
+			fc->ve_final_opts = kstrdup(options, GFP_KERNEL);
+			if (!fc->ve_final_opts) {
+				if (options != options_orig)
+					free_page((unsigned long)options);
+				return -ENOMEM;
+			}
+		}
 	}
 
 	/*
@@ -614,6 +803,7 @@ struct fs_context *vfs_dup_fs_context(struct fs_context *src_fc)
 	fc->s_fs_info	= NULL;
 	fc->source	= NULL;
 	fc->security	= NULL;
+	fc->ve_final_opts = NULL;
 	get_filesystem(fc->fs_type);
 	get_net(fc->net_ns);
 	get_user_ns(fc->user_ns);
@@ -742,6 +932,7 @@ void put_fs_context(struct fs_context *fc)
 	put_filesystem(fc->fs_type);
 	if (fc->lazy_opts)
 		free_page((unsigned long)fc->lazy_opts);
+	kfree(fc->ve_final_opts);
 	kfree(fc->source);
 	kfree(fc);
 }
@@ -962,6 +1153,8 @@ void vfs_clean_context(struct fs_context *fc)
 		free_page((unsigned long)fc->lazy_opts);
 		fc->lazy_opts = NULL;
 	}
+	kfree(fc->ve_final_opts);
+	fc->ve_final_opts = NULL;
 	kfree(fc->source);
 	fc->source = NULL;
 	fc->exclusive = false;
diff --git a/fs/internal.h b/fs/internal.h
index 3647ce69b2c7..f1892959db48 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -46,6 +46,14 @@ extern void __init chrdev_init(void);
  */
 extern const struct fs_context_operations legacy_fs_context_ops;
 extern int parse_monolithic_mount_data(struct fs_context *, void *);
+#ifdef CONFIG_VE
+extern int ve_devmnt_verify_fc(struct fs_context *fc, bool new_mount);
+#else
+static inline int ve_devmnt_verify_fc(struct fs_context *fc, bool new_mount)
+{
+	return 0;
+}
+#endif
 extern void vfs_clean_context(struct fs_context *fc);
 extern int finish_clean_context(struct fs_context *fc);
 
diff --git a/fs/namespace.c b/fs/namespace.c
index 43493f779c59..4d4dc5290350 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -3258,6 +3258,92 @@ int ve_devmnt_process(struct ve_struct *ve, dev_t dev, void **data_pp, int remou
 	return err;
 }
 
+/* Return 0 if every option in @options is listed in @a or @b, else -EPERM. */
+static int ve_devmnt_options_subset(char *options, char *a, char *b)
+{
+	char *copy, *cur, *p;
+	int err = 0;
+
+	if (!options || !*options)
+		return 0;
+	if (!a && !b)
+		return -EPERM;
+
+	copy = cur = kstrdup(options, GFP_KERNEL);
+	if (!copy)
+		return -ENOMEM;
+
+	while ((p = strsep(&cur, ",")) != NULL) {
+		if (!*p)
+			continue;
+		if ((!a || !strstr_separated(a, p, ',')) &&
+		    (!b || !strstr_separated(b, p, ','))) {
+			err = -EPERM;
+			break;
+		}
+	}
+
+	kfree(copy);
+	return err;
+}
+
+/*
+ * ve_devmnt_verify - enforce the container device-mount policy for @dev
+ * @ve: the container
+ * @dev: device taken from the mounted superblock (not from a raceable path)
+ * @opts: mount options plus the SB_* flag names to vet
+ * @new_mount: true for a new mount, false for a remount
+ * @have_user_opts: true if userspace supplied any mount options. @opts always
+ *	carries the kernel-synthesized ro/rw flag token, so it is never empty
+ *	and cannot answer this on its own.
+ *
+ * Every supplied option must be allowed or forced. On a new mount the forced
+ * ("hidden") options must also be present: a filesystem that skipped inserting
+ * them is refused rather than silently dropping a container's mandated option
+ */
+int ve_devmnt_verify(struct ve_struct *ve, dev_t dev, char *opts, bool new_mount,
+		     bool have_user_opts)
+{
+	struct ve_devmnt *devmnt;
+	char *allowed = NULL, *hidden = NULL;
+	bool found = false;
+	int err = 0;
+
+	if (ve->is_pseudosuper)
+		return 0;
+
+	mutex_lock(&ve->devmnt_mutex);
+	list_for_each_entry(devmnt, &ve->devmnt_list, link) {
+		if (devmnt->dev == dev) {
+			allowed = devmnt->allowed_options;
+			hidden = devmnt->hidden_options;
+			found = true;
+			break;
+		}
+	}
+
+	/*
+	 * Enforce for a listed device, or for any mount carrying userspace
+	 * options. An unlisted device with no userspace options is unconstrained
+	 * here, so the synthesized ro/rw token in @opts does not deny it.
+	 */
+	if (found || have_user_opts) {
+		/* every supplied option must be either allowed or forced */
+		err = ve_devmnt_options_subset(opts, allowed, hidden);
+
+		/* on a new mount every forced option must have reached the fs */
+		if (!err && new_mount)
+			err = ve_devmnt_options_subset(hidden, opts, NULL);
+	}
+	mutex_unlock(&ve->devmnt_mutex);
+
+	if (err == -EPERM)
+		ve_pr_warn_ratelimited(VE_LOG_BOTH,
+				       "VE%s: mount options not permitted for device %u:%u\n",
+				       ve_name(ve), MAJOR(dev), MINOR(dev));
+	return err;
+}
+
 static inline int ve_mount_allowed(void)
 {
 	struct ve_struct *ve = get_exec_env();
@@ -3308,23 +3394,6 @@ static inline void ve_mount_nr_inc(struct mount *mnt, struct ve_struct *ve) { }
 static inline void ve_mount_nr_dec(struct mount *mnt) { }
 #endif /* CONFIG_VE */
 
-static int ve_prepare_mount_options(struct fs_context *fc, void *data)
-{
-#ifdef CONFIG_VE
-	struct super_block *sb = fc->root->d_sb;
-	struct ve_struct *ve = get_exec_env();
-
-	if (sb->s_bdev && data && !ve_is_super(ve)) {
-		int err;
-
-		err = ve_devmnt_process(ve, sb->s_bdev->bd_dev, &data, 1);
-		if (err)
-			return err;
-	}
-#endif
-	return 0;
-}
-
 /*
  * change filesystem flags. dir should be a physical root of filesystem.
  * If you've mounted a non-root directory somewhere and want to do remount
@@ -3357,12 +3426,6 @@ static int do_remount(struct path *path, int ms_flags, int sb_flags,
 	 */
 	fc->oldapi = true;
 
-	err = ve_prepare_mount_options(fc, data);
-	if (err) {
-		put_fs_context(fc);
-		return err;
-	}
-
 	err = parse_monolithic_mount_data(fc, data);
 	if (!err) {
 		down_write(&sb->s_umount);
@@ -3816,6 +3879,7 @@ static int do_new_mount(struct path *path, const char *fstype, int sb_flags,
 					  subtype, strlen(subtype));
 	if (!err && name)
 		err = vfs_parse_fs_string(fc, "source", name, strlen(name));
+	/* Container device-mount policy is enforced later, in vfs_get_tree(). */
 	if (!err)
 		err = parse_monolithic_mount_data(fc, data);
 	if (!err && !mount_capable(fc))
diff --git a/fs/super.c b/fs/super.c
index 1adebbf35803..c0c067eb2d8e 100644
--- a/fs/super.c
+++ b/fs/super.c
@@ -1085,6 +1085,11 @@ int reconfigure_super(struct fs_context *fc)
 	if (retval)
 		return retval;
 
+	/* Enforce the container device-mount policy on the remount options. */
+	retval = ve_devmnt_verify_fc(fc, false);
+	if (retval)
+		return retval;
+
 	if (fc->sb_flags_mask & SB_RDONLY) {
 #ifdef CONFIG_BLOCK
 		if (!(fc->sb_flags & SB_RDONLY) && sb->s_bdev &&
@@ -1924,6 +1929,13 @@ int vfs_get_tree(struct fs_context *fc)
 		return error;
 	}
 
+	/* Enforce the container device-mount policy against the real device. */
+	error = ve_devmnt_verify_fc(fc, true);
+	if (unlikely(error)) {
+		fc_drop_locked(fc);
+		return error;
+	}
+
 	/*
 	 * filesystems should never set s_maxbytes larger than MAX_LFS_FILESIZE
 	 * but s_maxbytes was an unsigned long long for many releases. Throw
diff --git a/include/linux/fs_context.h b/include/linux/fs_context.h
index 1801aed1da67..2ca586e2cc2a 100644
--- a/include/linux/fs_context.h
+++ b/include/linux/fs_context.h
@@ -93,6 +93,8 @@ struct fs_context {
 	struct file_system_type	*fs_type;
 	void			*fs_private;	/* The filesystem's context */
 	void			*lazy_opts;	/* mount options which can't be checked at fsconfig() time */
+	/* option string handed to the fs, for the ve_devmnt policy check */
+	char			*ve_final_opts;
 	void			*sget_key;
 	struct dentry		*root;		/* The root and superblock */
 	struct user_namespace	*user_ns;	/* The user namespace for this mount */
diff --git a/include/linux/mount.h b/include/linux/mount.h
index 0cbc6f6893c0..ab0ea4f7afc6 100644
--- a/include/linux/mount.h
+++ b/include/linux/mount.h
@@ -127,5 +127,7 @@ extern int cifs_root_data(char **dev, char **opts);
 
 struct ve_struct;
 extern int ve_devmnt_process(struct ve_struct *, dev_t, void **, int);
+extern int ve_devmnt_verify(struct ve_struct *ve, dev_t dev, char *opts,
+			    bool new_mount, bool have_user_opts);
 
 #endif /* _LINUX_MOUNT_H */
-- 
2.43.0


^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH RHEL10 COMMIT] ms/qede: sync udp_tunnel ports outside qede_lock in the recovery path
       [not found]     <20260731155337.1209007-1-den@openvz.org>
@ 2026-08-05 11:20  3% ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-05 11:20 UTC (permalink / raw)


The commit is pushed to "branch-rh10-6.12.0-211.39.1.16.x.vz10-ovz" and will appear at git at bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.39.1.16.2.vz10
------>
commit b8673a452aa2f5ad79d424888140d11639ab0384
Author: Denis V. Lunev <den@openvz.org>
Date:   Fri Jul 31 17:53:37 2026 +0200

    ms/qede: sync udp_tunnel ports outside qede_lock in the recovery path
    
    A TX timeout on a qede NIC that has VXLAN/GENEVE tunnel ports
    configured wedges the rtnetlink control plane of the whole machine:
    
      NETDEV WATCHDOG: ens6f1 (qede): transmit queue 2 timed out 10226 ms
      [qede_tx_timeout:586(ens6f1)]TX timeout on queue 2!
      [qede_recovery_handler:2665(ens6f0)]Starting a recovery process
    
    The recovery path deadlocks on the driver's own mutex:
    
      qede_sp_task
       rtnl_lock()
       mutex_lock(&edev->qede_lock)        <- taken
       qede_recovery_handler
        qede_load
        udp_tunnel_nic_reset_ntf
         __udp_tunnel_nic_device_sync
          info->sync_table == qede_udp_tunnel_sync
           mutex_lock(&edev->qede_lock)    <- same task: deadlock
    
    The mutex is not recursive, so the kworker blocks on itself with
    rtnl_lock held, and neither lock is ever released. Every task that
    calls rtnl_lock() afterwards (ip, ovs-vswitchd, lldpad, IPv6
    addrconf, sshd) blocks forever while the node still answers ping.
    In a vmcore from an affected production node rtnl_mutex.owner
    decodes to the very kworker blocked at the innermost mutex_lock()
    above.
    
    Re-sync the tunnel ports from qede_sp_task() after the internal lock
    is dropped, still under rtnl_lock as the udp_tunnel API requires.
    This mirrors qede_open(), which calls udp_tunnel_nic_reset_ntf()
    under rtnl without the internal lock.
    
    qede_recovery_handler() now returns whether it has successfully
    reloaded an open device, and the caller re-syncs the ports only in
    that case. This keeps the old gating exactly: a device that was down
    or a failed recovery returns false, as those paths never reached the
    udp_tunnel_nic_reset_ntf() call before either.
    
    This was the only user of the qede_lock()/qede_unlock() helpers, so
    remove them.
    
    Fixes: 8cd160a29415 ("qede: convert to new udp_tunnel_nic infra")
    Signed-off-by: Denis V. Lunev <den@openvz.org>
    
    CC: Andrew Lunn <andrew+netdev@lunn.ch>
    CC: "David S. Miller" <davem@davemloft.net>
    CC: Eric Dumazet <edumazet@google.com>
    CC: Jakub Kicinski <kuba@kernel.org>
    CC: Paolo Abeni <pabeni@redhat.com>
    
    (cherry picked from commit 451c9075d6c53f2438d110addbeeeea6fac18567)
    https://virtuozzo.atlassian.net/browse/VSTOR-138358
    Feature: fix ms/drivers
---
 drivers/net/ethernet/qlogic/qede/qede_main.c | 44 ++++++++++++++--------------
 1 file changed, 22 insertions(+), 22 deletions(-)

diff --git a/drivers/net/ethernet/qlogic/qede/qede_main.c b/drivers/net/ethernet/qlogic/qede/qede_main.c
index 66ab1b9d65a1a..cdb20606048ae 100644
--- a/drivers/net/ethernet/qlogic/qede/qede_main.c
+++ b/drivers/net/ethernet/qlogic/qede/qede_main.c
@@ -107,7 +107,7 @@ static void qede_remove(struct pci_dev *pdev);
 static void qede_shutdown(struct pci_dev *pdev);
 static void qede_link_update(void *dev, struct qed_link_output *link);
 static void qede_schedule_recovery_handler(void *dev);
-static void qede_recovery_handler(struct qede_dev *edev);
+static bool qede_recovery_handler(struct qede_dev *edev);
 static void qede_schedule_hw_err_handler(void *dev,
 					 enum qed_hw_err_type err_type);
 static void qede_get_eth_tlv_data(void *edev, void *data);
@@ -1047,21 +1047,6 @@ void __qede_unlock(struct qede_dev *edev)
 	mutex_unlock(&edev->qede_lock);
 }
 
-/* This version of the lock should be used when acquiring the RTNL lock is also
- * needed in addition to the internal qede lock.
- */
-static void qede_lock(struct qede_dev *edev)
-{
-	rtnl_lock();
-	__qede_lock(edev);
-}
-
-static void qede_unlock(struct qede_dev *edev)
-{
-	__qede_unlock(edev);
-	rtnl_unlock();
-}
-
 static void qede_periodic_task(struct work_struct *work)
 {
 	struct qede_dev *edev = container_of(work, struct qede_dev,
@@ -1098,6 +1083,8 @@ static void qede_sp_task(struct work_struct *work)
 	 */
 
 	if (test_and_clear_bit(QEDE_SP_RECOVERY, &edev->sp_flags)) {
+		bool reloaded;
+
 		cancel_delayed_work_sync(&edev->periodic_task);
 #ifdef CONFIG_QED_SRIOV
 		/* SRIOV must be disabled outside the lock to avoid a deadlock.
@@ -1106,9 +1093,17 @@ static void qede_sp_task(struct work_struct *work)
 		if (pci_num_vf(edev->pdev))
 			qede_sriov_configure(edev->pdev, 0);
 #endif
-		qede_lock(edev);
-		qede_recovery_handler(edev);
-		qede_unlock(edev);
+		rtnl_lock();
+		__qede_lock(edev);
+		reloaded = qede_recovery_handler(edev);
+		__qede_unlock(edev);
+
+		/* The udp_tunnel core synchronously calls back into
+		 * qede_udp_tunnel_sync(), which takes the qede lock.
+		 */
+		if (reloaded)
+			udp_tunnel_nic_reset_ntf(edev->ndev);
+		rtnl_unlock();
 	}
 
 	__qede_lock(edev);
@@ -2649,9 +2644,13 @@ static void qede_recovery_failed(struct qede_dev *edev)
 		edev->ops->common->set_power_state(edev->cdev, PCI_D3hot);
 }
 
-static void qede_recovery_handler(struct qede_dev *edev)
+/* Returns true if an open device was successfully reloaded and its
+ * udp_tunnel ports need to be re-synced by the caller.
+ */
+static bool qede_recovery_handler(struct qede_dev *edev)
 {
 	u32 curr_state = edev->state;
+	bool reloaded = false;
 	int rc;
 
 	DP_NOTICE(edev, "Starting a recovery process\n");
@@ -2681,17 +2680,18 @@ static void qede_recovery_handler(struct qede_dev *edev)
 			goto err;
 
 		qede_config_rx_mode(edev->ndev);
-		udp_tunnel_nic_reset_ntf(edev->ndev);
+		reloaded = true;
 	}
 
 	edev->state = curr_state;
 
 	DP_NOTICE(edev, "Recovery handling is done\n");
 
-	return;
+	return reloaded;
 
 err:
 	qede_recovery_failed(edev);
+	return false;
 }
 
 static void qede_atomic_hw_err_handler(struct qede_dev *edev)

^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH vz10 v8 1/1] fs: enforce container device-mount policy in the common mount path
  2026-08-04 12:53  3% [Devel] [PATCH vz10 v8 1/1] fs: enforce container device-mount policy in the common mount path Vasileios Almpanis
@ 2026-08-05 16:41  0% ` Pavel Tikhomirov
  2026-08-06 15:49  3% ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Pavel Tikhomirov @ 2026-08-05 16:41 UTC (permalink / raw)


Reviewed-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>

On 8/4/26 14:53, Vasileios Almpanis wrote:
> In a container the per-device ve_devmnt policy restricts which options
> a device may be mounted with and force-inserts a set of hidden options.
> The check used to run inside the option-string parser
> (vfs_parse_monolithic_sep) and, for remount, in a separate helper. Two
> things escaped it:
> 
>   - MS_* flags from the legacy mount(2)/fsconfig(2) API are folded into
>     fc->sb_flags and never appear in the option string, so a container
>     could set MS_RDONLY, MS_SYNCHRONOUS, MS_MANDLOCK, ... outside its
>     allowed set.
> 
>   - The check sat in filesystem-selectable callbacks (->parse_monolithic,
>     ->mount), so a filesystem not routing through them evaded the policy,
>     and a skipped hidden-option insertion dropped a container's mandated
>     options without error.
> 
> Enforce the policy in the fs-agnostic common mount path instead:
> vfs_get_tree() for a new mount and reconfigure_super() for a remount.
> The device is taken from the mounted superblock, so fc->source cannot be
> raced to target another device, and fc->sb_flags is vetted alongside the
> option string. On a new mount the forced options must also be present,
> so a filesystem that skipped inserting them has its mount refused rather
> than silently losing them.
> 
> The parse-time ve_devmnt_process() call is kept as a best-effort early
> reject, so a disallowed device or option is refused before the
> filesystem's fill_super() runs.
> 
> Fix a bug where a containers that use the legacy mount(2) syscall are able
> to reconfigure a mount and change the superblock flags, for example
> from RO to RW. Compute the effective superblock flags and emit rw incase
> SB_RDONLY is missing in vfs_format_sb_flags.
> 
> Explicitly deny mounts for device backed legacy filesystems. So even
> if FS_VIRTUALISED is added to them, mounting will not succeed since
> legacy filesystems don't go through vfs_parse_monolithic_sep and the
> policy is not applied to them. Keep this check until all filesystems
> have migrated to the new mount API.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-132330
> Fixes: 263467c864c5 ("ve/fs/devmnt: process mount options")
> Signed-off-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
> Co-developed-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
> Signed-off-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
> 
> Feature: ve: ve generic structures
> ---
> 
> Changes since v7:
> - Add ve_devmnt_deny_legacy called by ve_devmnt_verify_fc only for
>   mounts that required a block device and for non super VEs. With a
>   comment explaining the rationale behind it, and for it to be removed
>   in the future when all filesystem types have migrated to the new mount
>   API namely when our kernels base becomes newer than v6.15. This will
>   deny any mount in containers that are trying to use legacy filesystem
>   types like ext2 even if some developer adds the FS_VIRTUALISED flag
>   and recomplies the kernel with a warning.
> 
> Changes since v6:
>  - Enforce sb-flag changes on remount: emit "rw" when SB_RDONLY is cleared
>    and vet the effective flags,
>    (sb->s_flags & ~fc->sb_flags_mask) | (fc->sb_flags & fc->sb_flags_mask),
>    so a legacy mount(2) remount can no longer flip RO->RW unchecked.
>  - Constrain only devices listed in ve.mount_opts: an unlisted device
>    mounted with no userspace options is allowed rather than refused by the
>    synthesized ro/rw token (tracked via have_user_opts).
>  - Reset fc->ve_final_opts in vfs_dup_fs_context() to avoid a double-free.
>  - Factor per-option emit into __vfs_emit_flag(); checkpatch/style fixes.
> 
> Changes since v5:
>  - Fix use-after-free: do_new_mount() freed the merged page that
>    legacy_get_tree()->mount() dereferences later; the fs now sees only
>    the caller-owned original data.
>  - Fix remount,dirsync failing with -EINVAL in containers: stop re-parsing
>    the formatted flag names (SB_DIRSYNC is not in MS_RMT_MASK).
>  - Check policy only: legacy fs parsers no longer get synthesized ro/sync
>    tokens, and the flag check reads fc->sb_flags symmetrically for mount
>    and fsconfig.
>  - Keep ve_check_mount_options() only on the remount path (pinned sb
>    device, covers legacy ->reconfigure); drop legacy_merge_mount_data()
>    and ve_prepare_mount_options(); add Fixes: tag and rewrite the message.
> 
> Changes since v4:
>  - Emit only the positive sb-flag names, not the clear names (rw/async/
>    ...); on legacy remount sb_flags_mask is MS_RMT_MASK, so the clear
>    names had wrongly rejected ordinary in-container remounts.
>  - NUL-terminate the options page when data is empty and no flags emitted.
>  - Fix __vfs_format_flags() comment (-E2BIG, not -ENOSPC).
> 
> Changes since v3:
>  - Drop excess length check in legacy_merge_mount_data().
> 
> Changes since v2:
>  - Remove the legacy_merge_mount_data guard in fs/internal.h.
>  - Add __vfs_format_flags() helper, used by vfs_format_sb_flags().
>  - Use -E2BIG (not -ENOSPC) for the buffer-too-small case.
> 
> Changes since v1:
>  - Unify the comma-insert-copy pattern across call sites via an
>    append_entry() helper.
>  - Rework legacy_merge_mount_data() to allocate the page upfront and
>    append sb flags via vfs_format_sb_flags(), dropping flags_buf[128]
>    and the size arithmetic.
>  - Use -ENOSPC (not -EINVAL) for buffer-too-small; comment
>    FS_BINARY_MOUNTDATA; minor blank-line cleanups.
> 
>  fs/fs_context.c            | 193 +++++++++++++++++++++++++++++++++++++
>  fs/internal.h              |   8 ++
>  fs/namespace.c             | 110 ++++++++++++++++-----
>  fs/super.c                 |  12 +++
>  include/linux/fs_context.h |   2 +
>  include/linux/mount.h      |   2 +
>  6 files changed, 304 insertions(+), 23 deletions(-)
> 
> diff --git a/fs/fs_context.c b/fs/fs_context.c
> index 76f34f3d468e..b2bd21a42083 100644
> --- a/fs/fs_context.c
> +++ b/fs/fs_context.c
> @@ -81,6 +81,70 @@ static int vfs_parse_sb_flag(struct fs_context *fc, const char *key)
>  	return -ENOPARAM;
>  }
>  
> +/*
> + * Emit option @name into @buff at *@off, prefixed with ',' if the buffer
> + * already holds text. Advances *@off. Returns 0 or -E2BIG if @buff is full.
> + */
> +static int __vfs_emit_flag(const char *name, char *buff,
> +			   size_t size, size_t *off)
> +{
> +	ssize_t ret;
> +
> +	if (*off) {
> +		if (*off + 1 >= size)
> +			return -E2BIG;
> +		buff[(*off)++] = ',';
> +	}
> +
> +	ret = strscpy(buff + *off, name, size - *off);
> +	if (ret < 0)
> +		return -E2BIG;
> +	*off += ret;
> +	return 0;
> +}
> +
> +static int __vfs_format_flags(const struct constant_table *p, unsigned int flags,
> +			      char *buff, size_t size, size_t *off)
> +{
> +	for (; p->name; p++) {
> +		int ret;
> +
> +		if (!(flags & p->value))
> +			continue;
> +		ret = __vfs_emit_flag(p->name, buff, size, off);
> +		if (ret)
> +			return ret;
> +	}
> +	return 0;
> +}
> +
> +static int vfs_format_sb_flags(char *buff, size_t size, size_t *off,
> +			       unsigned int sb_flags)
> +{
> +	int err;
> +
> +	err = __vfs_format_flags(common_set_sb_flag, sb_flags, buff, size, off);
> +	if (err)
> +		return err;
> +
> +	/*
> +	 * "rw" has no flag bit of its own - it is simply the absence of
> +	 * SB_RDONLY. Emit it explicitly so the ve_devmnt policy can allow or
> +	 * deny read-write access as a first-class option; otherwise a mount or
> +	 * remount that leaves the superblock read-write carries no token and
> +	 * slips past the "every option must be allowed" check.
> +	 *
> +	 * @sb_flags is the effective post-operation flag word, so this reflects
> +	 * the state the superblock actually ends up in. A remount that only
> +	 * touches an unrelated flag (e.g. "sync") keeps its current SB_RDONLY
> +	 * and so does not emit "rw".
> +	 */
> +	if (!(sb_flags & SB_RDONLY))
> +		return __vfs_emit_flag("rw", buff, size, off);
> +
> +	return 0;
> +}
> +
>  /**
>   * vfs_parse_fs_param_source - Handle setting "source" via parameter
>   * @fc: The filesystem context to modify
> @@ -224,6 +288,119 @@ static inline int fscontext_lookup_bdev(struct fs_context *fc, dev_t *s_dev)
>  	return -ENODEV;
>  }
>  
> +/*
> + * ve_devmnt_deny_legacy - refuse a device mount that escapes the policy
> + * @fc: the mount context
> + *
> + * A legacy context, one for a filesystem with no ->init_fs_context passes
> + * its mount data straight to ->mount()/->remount_fs() through
> + * legacy_parse_monolithic(), so vfs_parse_monolithic_sep() never runs on it.
> + * The container's forced options are then never inserted and fc->ve_final_opts
> + * stays empty, which would leave ve_devmnt_verify_fc() vetting an empty option
> + * string while the filesystem acts on the full one userspace passed.
> + *
> + * Returns 0 if the mount may go ahead, -EPERM if it must not.
> + */
> +static int ve_devmnt_deny_legacy(struct fs_context *fc)
> +{
> +	struct ve_struct *ve = get_exec_env();
> +
> +	if (fc->ops != &legacy_fs_context_ops)
> +		return 0;
> +
> +	ve_pr_warn_ratelimited(VE_LOG_BOTH,
> +			       "VE%s: refusing to mount %s: filesystem has no fs_context support\n",
> +			       ve_name(ve), fc->fs_type->name);
> +	return -EPERM;
> +}
> +
> +/*
> + * ve_devmnt_verify_fc - check a mount against the container device-mount policy
> + * @fc: the mount context, with fc->root set
> + * @new_mount: true at vfs_get_tree() (new mount), false at reconfigure_super()
> + *
> + * Vets the stashed userspace option string plus the synthesized SB_* flag
> + * names against the mounted superblock's device. A device absent from the
> + * policy that is mounted with no userspace options is allowed. Returns 0 when
> + * permitted (or no check applies), or a negative errno.
> + */
> +int ve_devmnt_verify_fc(struct fs_context *fc, bool new_mount)
> +{
> +	struct ve_struct *ve = get_exec_env();
> +	unsigned int sb_flags;
> +	bool have_user_opts;
> +	size_t off = 0;
> +	char *page;
> +	int err;
> +
> +	if (ve_is_super(ve))
> +		return 0;
> +
> +	if (!fc->fs_type || !(fc->fs_type->fs_flags & FS_REQUIRES_DEV))
> +		return 0;
> +
> +	/*
> +	 * Keep this check until all filesystems have migrated to the new
> +	 * mount API
> +	 */
> +	err = ve_devmnt_deny_legacy(fc);
> +	if (err)
> +		return err;
> +
> +	/*
> +	 * Filesystems with binary mount data (e.g. btrfs) bypass option
> +	 * string parsing entirely, so our checks cannot apply here.
> +	 */
> +	if (fc->fs_type->fs_flags & FS_BINARY_MOUNTDATA)
> +		return 0;
> +
> +	if (WARN_ON_ONCE(!fc->root))
> +		return -EINVAL;
> +
> +	page = (char *)__get_free_page(GFP_KERNEL);
> +	if (!page)
> +		return -ENOMEM;
> +
> +	/*
> +	 * Track whether userspace actually supplied options. @page below also
> +	 * gets the synthesized ro/rw flag token, so its length cannot answer
> +	 * this; ve_final_opts holds only the userspace string.
> +	 */
> +	have_user_opts = fc->ve_final_opts && *fc->ve_final_opts;
> +	if (have_user_opts) {
> +		ssize_t ret = strscpy(page, fc->ve_final_opts, PAGE_SIZE);
> +
> +		if (ret < 0) {
> +			err = -E2BIG;
> +			goto out;
> +		}
> +		off = ret;
> +	}
> +
> +	/*
> +	 * On a remount fc->sb_flags holds only the bits being changed, so
> +	 * combine them with the current superblock flags to get the state the
> +	 * sb will actually have - the same value reconfigure_super() writes
> +	 * back. On a new mount fc->sb_flags is already the full flag word.
> +	 */
> +	sb_flags = fc->sb_flags;
> +	if (!new_mount)
> +		sb_flags = (fc->root->d_sb->s_flags & ~fc->sb_flags_mask) |
> +			   (fc->sb_flags & fc->sb_flags_mask);
> +
> +	err = vfs_format_sb_flags(page, PAGE_SIZE, &off, sb_flags);
> +	if (err)
> +		goto out;
> +
> +	page[off] = '\0';
> +	err = ve_devmnt_verify(ve, fc->root->d_sb->s_dev, page, new_mount,
> +			       have_user_opts);
> +
> +out:
> +	free_page((unsigned long)page);
> +	return err;
> +}
> +
>  static int fscontext_init_lazy_opts(struct fs_context *fc)
>  {
>  	struct ve_struct *ve = get_exec_env();
> @@ -389,10 +566,22 @@ int vfs_parse_monolithic_sep(struct fs_context *fc, void *data,
>  			return -ENODEV;
>  		}
>  
> +		/* Early reject and hidden-option insertion; verified for real later. */
>  		ret = ve_devmnt_process(ve, bd_dev, (void **) &options,
>  				fc->purpose == FS_CONTEXT_FOR_RECONFIGURE);
>  		if (ret)
>  			return ret;
> +
> +		/* Stash what the filesystem parses; checked in the common mount path. */
> +		if (options) {
> +			kfree(fc->ve_final_opts);
> +			fc->ve_final_opts = kstrdup(options, GFP_KERNEL);
> +			if (!fc->ve_final_opts) {
> +				if (options != options_orig)
> +					free_page((unsigned long)options);
> +				return -ENOMEM;
> +			}
> +		}
>  	}
>  
>  	/*
> @@ -614,6 +803,7 @@ struct fs_context *vfs_dup_fs_context(struct fs_context *src_fc)
>  	fc->s_fs_info	= NULL;
>  	fc->source	= NULL;
>  	fc->security	= NULL;
> +	fc->ve_final_opts = NULL;
>  	get_filesystem(fc->fs_type);
>  	get_net(fc->net_ns);
>  	get_user_ns(fc->user_ns);
> @@ -742,6 +932,7 @@ void put_fs_context(struct fs_context *fc)
>  	put_filesystem(fc->fs_type);
>  	if (fc->lazy_opts)
>  		free_page((unsigned long)fc->lazy_opts);
> +	kfree(fc->ve_final_opts);
>  	kfree(fc->source);
>  	kfree(fc);
>  }
> @@ -962,6 +1153,8 @@ void vfs_clean_context(struct fs_context *fc)
>  		free_page((unsigned long)fc->lazy_opts);
>  		fc->lazy_opts = NULL;
>  	}
> +	kfree(fc->ve_final_opts);
> +	fc->ve_final_opts = NULL;
>  	kfree(fc->source);
>  	fc->source = NULL;
>  	fc->exclusive = false;
> diff --git a/fs/internal.h b/fs/internal.h
> index 3647ce69b2c7..f1892959db48 100644
> --- a/fs/internal.h
> +++ b/fs/internal.h
> @@ -46,6 +46,14 @@ extern void __init chrdev_init(void);
>   */
>  extern const struct fs_context_operations legacy_fs_context_ops;
>  extern int parse_monolithic_mount_data(struct fs_context *, void *);
> +#ifdef CONFIG_VE
> +extern int ve_devmnt_verify_fc(struct fs_context *fc, bool new_mount);
> +#else
> +static inline int ve_devmnt_verify_fc(struct fs_context *fc, bool new_mount)
> +{
> +	return 0;
> +}
> +#endif
>  extern void vfs_clean_context(struct fs_context *fc);
>  extern int finish_clean_context(struct fs_context *fc);
>  
> diff --git a/fs/namespace.c b/fs/namespace.c
> index 43493f779c59..4d4dc5290350 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
> @@ -3258,6 +3258,92 @@ int ve_devmnt_process(struct ve_struct *ve, dev_t dev, void **data_pp, int remou
>  	return err;
>  }
>  
> +/* Return 0 if every option in @options is listed in @a or @b, else -EPERM. */
> +static int ve_devmnt_options_subset(char *options, char *a, char *b)
> +{
> +	char *copy, *cur, *p;
> +	int err = 0;
> +
> +	if (!options || !*options)
> +		return 0;
> +	if (!a && !b)
> +		return -EPERM;
> +
> +	copy = cur = kstrdup(options, GFP_KERNEL);
> +	if (!copy)
> +		return -ENOMEM;
> +
> +	while ((p = strsep(&cur, ",")) != NULL) {
> +		if (!*p)
> +			continue;
> +		if ((!a || !strstr_separated(a, p, ',')) &&
> +		    (!b || !strstr_separated(b, p, ','))) {
> +			err = -EPERM;
> +			break;
> +		}
> +	}
> +
> +	kfree(copy);
> +	return err;
> +}
> +
> +/*
> + * ve_devmnt_verify - enforce the container device-mount policy for @dev
> + * @ve: the container
> + * @dev: device taken from the mounted superblock (not from a raceable path)
> + * @opts: mount options plus the SB_* flag names to vet
> + * @new_mount: true for a new mount, false for a remount
> + * @have_user_opts: true if userspace supplied any mount options. @opts always
> + *	carries the kernel-synthesized ro/rw flag token, so it is never empty
> + *	and cannot answer this on its own.
> + *
> + * Every supplied option must be allowed or forced. On a new mount the forced
> + * ("hidden") options must also be present: a filesystem that skipped inserting
> + * them is refused rather than silently dropping a container's mandated option
> + */
> +int ve_devmnt_verify(struct ve_struct *ve, dev_t dev, char *opts, bool new_mount,
> +		     bool have_user_opts)
> +{
> +	struct ve_devmnt *devmnt;
> +	char *allowed = NULL, *hidden = NULL;
> +	bool found = false;
> +	int err = 0;
> +
> +	if (ve->is_pseudosuper)
> +		return 0;
> +
> +	mutex_lock(&ve->devmnt_mutex);
> +	list_for_each_entry(devmnt, &ve->devmnt_list, link) {
> +		if (devmnt->dev == dev) {
> +			allowed = devmnt->allowed_options;
> +			hidden = devmnt->hidden_options;
> +			found = true;
> +			break;
> +		}
> +	}
> +
> +	/*
> +	 * Enforce for a listed device, or for any mount carrying userspace
> +	 * options. An unlisted device with no userspace options is unconstrained
> +	 * here, so the synthesized ro/rw token in @opts does not deny it.
> +	 */
> +	if (found || have_user_opts) {
> +		/* every supplied option must be either allowed or forced */
> +		err = ve_devmnt_options_subset(opts, allowed, hidden);
> +
> +		/* on a new mount every forced option must have reached the fs */
> +		if (!err && new_mount)
> +			err = ve_devmnt_options_subset(hidden, opts, NULL);
> +	}
> +	mutex_unlock(&ve->devmnt_mutex);
> +
> +	if (err == -EPERM)
> +		ve_pr_warn_ratelimited(VE_LOG_BOTH,
> +				       "VE%s: mount options not permitted for device %u:%u\n",
> +				       ve_name(ve), MAJOR(dev), MINOR(dev));
> +	return err;
> +}
> +
>  static inline int ve_mount_allowed(void)
>  {
>  	struct ve_struct *ve = get_exec_env();
> @@ -3308,23 +3394,6 @@ static inline void ve_mount_nr_inc(struct mount *mnt, struct ve_struct *ve) { }
>  static inline void ve_mount_nr_dec(struct mount *mnt) { }
>  #endif /* CONFIG_VE */
>  
> -static int ve_prepare_mount_options(struct fs_context *fc, void *data)
> -{
> -#ifdef CONFIG_VE
> -	struct super_block *sb = fc->root->d_sb;
> -	struct ve_struct *ve = get_exec_env();
> -
> -	if (sb->s_bdev && data && !ve_is_super(ve)) {
> -		int err;
> -
> -		err = ve_devmnt_process(ve, sb->s_bdev->bd_dev, &data, 1);
> -		if (err)
> -			return err;
> -	}
> -#endif
> -	return 0;
> -}
> -
>  /*
>   * change filesystem flags. dir should be a physical root of filesystem.
>   * If you've mounted a non-root directory somewhere and want to do remount
> @@ -3357,12 +3426,6 @@ static int do_remount(struct path *path, int ms_flags, int sb_flags,
>  	 */
>  	fc->oldapi = true;
>  
> -	err = ve_prepare_mount_options(fc, data);
> -	if (err) {
> -		put_fs_context(fc);
> -		return err;
> -	}
> -
>  	err = parse_monolithic_mount_data(fc, data);
>  	if (!err) {
>  		down_write(&sb->s_umount);
> @@ -3816,6 +3879,7 @@ static int do_new_mount(struct path *path, const char *fstype, int sb_flags,
>  					  subtype, strlen(subtype));
>  	if (!err && name)
>  		err = vfs_parse_fs_string(fc, "source", name, strlen(name));
> +	/* Container device-mount policy is enforced later, in vfs_get_tree(). */
>  	if (!err)
>  		err = parse_monolithic_mount_data(fc, data);
>  	if (!err && !mount_capable(fc))
> diff --git a/fs/super.c b/fs/super.c
> index 1adebbf35803..c0c067eb2d8e 100644
> --- a/fs/super.c
> +++ b/fs/super.c
> @@ -1085,6 +1085,11 @@ int reconfigure_super(struct fs_context *fc)
>  	if (retval)
>  		return retval;
>  
> +	/* Enforce the container device-mount policy on the remount options. */
> +	retval = ve_devmnt_verify_fc(fc, false);
> +	if (retval)
> +		return retval;
> +
>  	if (fc->sb_flags_mask & SB_RDONLY) {
>  #ifdef CONFIG_BLOCK
>  		if (!(fc->sb_flags & SB_RDONLY) && sb->s_bdev &&
> @@ -1924,6 +1929,13 @@ int vfs_get_tree(struct fs_context *fc)
>  		return error;
>  	}
>  
> +	/* Enforce the container device-mount policy against the real device. */
> +	error = ve_devmnt_verify_fc(fc, true);
> +	if (unlikely(error)) {
> +		fc_drop_locked(fc);
> +		return error;
> +	}
> +
>  	/*
>  	 * filesystems should never set s_maxbytes larger than MAX_LFS_FILESIZE
>  	 * but s_maxbytes was an unsigned long long for many releases. Throw
> diff --git a/include/linux/fs_context.h b/include/linux/fs_context.h
> index 1801aed1da67..2ca586e2cc2a 100644
> --- a/include/linux/fs_context.h
> +++ b/include/linux/fs_context.h
> @@ -93,6 +93,8 @@ struct fs_context {
>  	struct file_system_type	*fs_type;
>  	void			*fs_private;	/* The filesystem's context */
>  	void			*lazy_opts;	/* mount options which can't be checked at fsconfig() time */
> +	/* option string handed to the fs, for the ve_devmnt policy check */
> +	char			*ve_final_opts;
>  	void			*sget_key;
>  	struct dentry		*root;		/* The root and superblock */
>  	struct user_namespace	*user_ns;	/* The user namespace for this mount */
> diff --git a/include/linux/mount.h b/include/linux/mount.h
> index 0cbc6f6893c0..ab0ea4f7afc6 100644
> --- a/include/linux/mount.h
> +++ b/include/linux/mount.h
> @@ -127,5 +127,7 @@ extern int cifs_root_data(char **dev, char **opts);
>  
>  struct ve_struct;
>  extern int ve_devmnt_process(struct ve_struct *, dev_t, void **, int);
> +extern int ve_devmnt_verify(struct ve_struct *ve, dev_t dev, char *opts,
> +			    bool new_mount, bool have_user_opts);
>  
>  #endif /* _LINUX_MOUNT_H */

-- 
Best regards, Pavel Tikhomirov
Senior Software Developer, Virtuozzo.


^ permalink raw reply	[relevance 0%]

* [Devel] [PATCH vz10 07/24] blk-cbt: don't WARN on a user-supplied ABI version mismatch
       [not found]       ` <970b9d91-59c9-4a6d-8494-2d58afcab4b8@virtuozzo.com>
@ 2026-08-05 19:26  0%     ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-05 19:26 UTC (permalink / raw)


On 7/6/26 14:11, Andrey Zhadchenko wrote:
> I don't like that. Customers do not run with panic_on_warn. If this 
> fails in our test environment, that's actually great (it means something 
> went very wrong). pr_warn_ratelimited is worse than WARN_ONCE regarding 
> intentional spamming.

i agree, i will drop this patch.
It could definitely be useful in case there was WARN() - to change it to WARN_ONCE()
because this is triggerable from inside a Container, i have just checked that.

But as this is just a single WARN_ONCE, it's not a problem.

On the other hand mainstream fights with such user triggerable WARN_ONCE messages as well, like

commit 251a8fe1b9aedccd298b77bc28426d564c5a923f
Author: Masami Hiramatsu (Google) <mhiramat@kernel.org>
Date:   Thu Jun 25 08:34:46 2026 +0900

    tracing/probes: Remove WARN_ON_ONCE from parse_btf_arg

    Sashiko found that user can cause this WARN_ON_ONCE() easily
    with adding a kprobe event based on a raw address with BTF
    parameter.

    Since this is not an unexpected condition, remove the
    WARN_ON_ONCE().

    Link: https://lore.kernel.org/all/178177265367.2059927.13789953014706792126.stgit at mhiramat.tok.corp.google.com/

    Link: https://sashiko.dev/#/patchset/178165816303.269421.7302603996990753309.stgit%40devnote2

    Reported-by: Sashiko <sashiko-bot@kernel.org>
    Fixes: b576e09701c7 ("tracing/probes: Support function parameters if BTF is available")
    Signed-off-by: Masami Hiramatsu (Google) <mhiramat@kernel.org>

diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c
index fd1caa1f97233..98532c503d028 100644
--- a/kernel/trace/trace_probe.c
+++ b/kernel/trace/trace_probe.c
@@ -678,7 +678,7 @@ static int parse_btf_arg(char *varname,
        int i, is_ptr, ret;
        u32 tid;

-       if (WARN_ON_ONCE(!ctx->funcname && !(ctx->flags & TPARG_FL_TEVENT)))
+       if (!ctx->funcname && !(ctx->flags & TPARG_FL_TEVENT))
                return -EINVAL;

        is_ptr = split_next_field(varname, &field, ctx);

commit 40c88c429a598006f91ad7a2b89856cd50b3a008
Author: Andrii Nakryiko <andrii@kernel.org>
Date:   Tue May 16 11:04:09 2023 -0700

    bpf: drop unnecessary user-triggerable WARN_ONCE in verifierl log

    [ Upstream commit cff36398bd4c7d322d424433db437f3c3391c491 ]

    It's trivial for user to trigger "verifier log line truncated" warning,
    as verifier has a fixed-sized buffer of 1024 bytes (as of now), and there are at
    least two pieces of user-provided information that can be output through
    this buffer, and both can be arbitrarily sized by user:
      - BTF names;
      - BTF.ext source code lines strings.

    Verifier log buffer should be properly sized for typical verifier state
    output. But it's sort-of expected that this buffer won't be long enough
    in some circumstances. So let's drop the check. In any case code will
    work correctly, at worst truncating a part of a single line output.

    Reported-by: syzbot+8b2a08dfbd25fd933d75 at syzkaller.appspotmail.com
    Signed-off-by: Andrii Nakryiko <andrii@kernel.org>
    Link: https://lore.kernel.org/r/20230516180409.3549088-1-andrii at kernel.org
    Signed-off-by: Alexei Starovoitov <ast@kernel.org>
    Signed-off-by: Sasha Levin <sashal@kernel.org>

diff --git a/kernel/bpf/log.c b/kernel/bpf/log.c
index 920061e38d2e1..cd1b7113fbfd0 100644
--- a/kernel/bpf/log.c
+++ b/kernel/bpf/log.c
@@ -22,9 +22,6 @@ void bpf_verifier_vlog(struct bpf_verifier_log *log, const char *fmt,

        n = vscnprintf(log->kbuf, BPF_VERIFIER_TMP_LOG_SIZE, fmt, args);

-       WARN_ONCE(n >= BPF_VERIFIER_TMP_LOG_SIZE - 1,
-                 "verifier log line truncated - local buffer too short\n");
-
        if (log->level == BPF_LOG_KERNEL) {
                bool newline = n > 0 && log->kbuf[n - 1] == '\n';


> On 7/6/26 12:59, Konstantin Khorenko wrote:
>> blk_cbt_ioctl() reads abi_version straight from the ioctl argument and
>> WARN_ONCE()s if it does not match CBT_ABI_VERSION. The value is fully
>> userspace-controlled, so any process issuing a BLKCBT* ioctl with a
>> stale/newer struct taints the kernel, dumps a backtrace, and can panic a
>> host that runs with panic_on_warn. Downgrade to pr_warn_ratelimited();
>> the -EOPNOTSUPP return is the actual contract.
>>
>> Fixes: 6e42f62a2c88 ("block/blk-cbt: introduce ABI versioning")
>> Feature: cbt: changed block tracking (for backup)
>> https://virtuozzo.atlassian.net/browse/VSTOR-137234
>> Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
>> ---
>>   block/blk-cbt.c | 4 ++--
>>   1 file changed, 2 insertions(+), 2 deletions(-)
>>
>> diff --git a/block/blk-cbt.c b/block/blk-cbt.c
>> index 90219f58f1ae..91a888770411 100644
>> --- a/block/blk-cbt.c
>> +++ b/block/blk-cbt.c
>> @@ -1093,8 +1093,8 @@ int blk_cbt_ioctl(struct block_device *bdev, unsigned cmd, char __user *arg)
>>   		return -EFAULT;
>>   
>>   	if (abi_version != CBT_ABI_VERSION) {
>> -		WARN_ONCE(1, "blk-cbt ABI mimatch: kernel has %d, userspace uses %d",
>> -			  CBT_ABI_VERSION, abi_version);
>> +		pr_warn_ratelimited("blk-cbt: ABI mismatch: kernel has %d, userspace uses %d\n",
>> +				    CBT_ABI_VERSION, abi_version);
>>   		return -EOPNOTSUPP;
>>   	}
>>   
> 


^ permalink raw reply	[relevance 0%]

* [Devel] [PATCH RHEL10 COMMIT] drivers/base/cpu: fix cpu/offline content inside a ve
       [not found]     <20260706110002.1024515-17-khorenko@virtuozzo.com>
@ 2026-08-05 20:05  5% ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-05 20:05 UTC (permalink / raw)


The commit is pushed to "branch-rh10-6.12.0-211.39.1.16.x.vz10-ovz" and will appear at git at bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.39.1.16.2.vz10
------>
commit fdfa48283061fcd77b59e4ba7d182d8af3e4198e
Author: Konstantin Khorenko <khorenko@virtuozzo.com>
Date:   Mon Jul 6 12:59:54 2026 +0200

    drivers/base/cpu: fix cpu/offline content inside a ve
    
    print_cpus_offline()'s non-super-ve early return passed len (declared
    0 at that point, used later only as the sysfs_emit_at() accumulator)
    as snprintf()'s size argument, so nothing was ever written to buf.
    Since sysfs zeroes the page before calling show(), reading
    /sys/devices/system/cpu/offline from inside a ve returned a single
    NUL byte instead of the intended empty line ("\n").
    
    Fixes: 44a8c49297f94 ("ve/cpu: handle sysfs attributes for CTs")
    Feature: sysfs: per-CT entries visibility and permissions configuration
    https://virtuozzo.atlassian.net/browse/VSTOR-137234
    Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
    Reviewed-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
---
 drivers/base/cpu.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/base/cpu.c b/drivers/base/cpu.c
index 1e992254b12c6..b7ddc8346603c 100644
--- a/drivers/base/cpu.c
+++ b/drivers/base/cpu.c
@@ -263,7 +263,7 @@ static ssize_t print_cpus_offline(struct device *dev,
 	cpumask_var_t offline;
 
 	if (!ve_is_super(get_exec_env()))
-		return snprintf(buf, len, "\n");
+		return sysfs_emit(buf, "\n");
 
 	/* display offline cpus < nr_cpu_ids */
 	if (!alloc_cpumask_var(&offline, GFP_KERNEL))

^ permalink raw reply	[relevance 5%]

* [Devel] [PATCH RHEL10 COMMIT] ve: fix NULL-deref / use-after-free in ve_create() error unwind
       [not found]     <20260706110002.1024515-19-khorenko@virtuozzo.com>
@ 2026-08-05 20:09  4% ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-05 20:09 UTC (permalink / raw)


The commit is pushed to "branch-rh10-6.12.0-211.39.1.16.x.vz10-ovz" and will appear at git at bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.39.1.16.2.vz10
------>
commit 7e884677eb9540f3dc2907fd9af753a5ef972a9a
Author: Konstantin Khorenko <khorenko@virtuozzo.com>
Date:   Mon Jul 6 12:59:56 2026 +0200

    ve: fix NULL-deref / use-after-free in ve_create() error unwind
    
    ve_create()'s error path ended with:
    
            err_lat:
                    kmem_cache_free(ve_cachep, ve);
            err_ve:
                    ve_set_state(ve, VE_STATE_STOPPED);
                    return ERR_PTR(err);
    
    ve_set_state() dereferences ve->state, but at err_ve @ve is either NULL
    (the kmem_cache_zalloc() failure jumps straight to err_ve) or already
    freed (the err_lat/err_log/err_vdso legs kmem_cache_free() @ve and then
    fall through to err_ve) - so every error exit of ve_create() is a NULL
    pointer dereference or a use-after-free. It only triggers when an
    allocation in ve_create() fails, which is why it stayed latent.
    
    The state assignment is meaningless on the failure path anyway: the css
    is never returned to cgroup and the ve is being freed. The success path
    already sets ve->state = VE_STATE_STARTING directly. Just drop the bogus
    ve_set_state() from the unwind.
    
    Fixes: 8a21e780ce29a ("ve/cgroups: rework is_running into an explicit VE state")
    Feature: ve: ve generic structures
    https://virtuozzo.atlassian.net/browse/VSTOR-137234
    Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
    Reviewed-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
---
 kernel/ve/ve.c | 1 -
 1 file changed, 1 deletion(-)

diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index d8ef28eedabd0..73d1c3b4873e5 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -802,7 +802,6 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
 err_lat:
 	kmem_cache_free(ve_cachep, ve);
 err_ve:
-	ve_set_state(ve, VE_STATE_STOPPED);
 	return ERR_PTR(err);
 }
 

^ permalink raw reply	[relevance 4%]

* [Devel] [PATCH RHEL10 COMMIT] fs: enforce container device-mount policy in the common mount path
  2026-08-04 12:53  3% [Devel] [PATCH vz10 v8 1/1] fs: enforce container device-mount policy in the common mount path Vasileios Almpanis
  2026-08-05 16:41  0% ` Pavel Tikhomirov
@ 2026-08-06 15:49  3% ` Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-06 15:49 UTC (permalink / raw)


The commit is pushed to "branch-rh10-6.12.0-211.39.1.16.x.vz10-ovz" and will appear at git at bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.39.1.16.3.vz10
------>
commit 37940d776226f393a8f9baf1e3c65fd4e792544c
Author: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
Date:   Tue Aug 4 12:53:56 2026 +0000

    fs: enforce container device-mount policy in the common mount path
    
    In a container the per-device ve_devmnt policy restricts which options
    a device may be mounted with and force-inserts a set of hidden options.
    The check used to run inside the option-string parser
    (vfs_parse_monolithic_sep) and, for remount, in a separate helper. Two
    things escaped it:
    
      - MS_* flags from the legacy mount(2)/fsconfig(2) API are folded into
        fc->sb_flags and never appear in the option string, so a container
        could set MS_RDONLY, MS_SYNCHRONOUS, MS_MANDLOCK, ... outside its
        allowed set.
    
      - The check sat in filesystem-selectable callbacks (->parse_monolithic,
        ->mount), so a filesystem not routing through them evaded the policy,
        and a skipped hidden-option insertion dropped a container's mandated
        options without error.
    
    Enforce the policy in the fs-agnostic common mount path instead:
    vfs_get_tree() for a new mount and reconfigure_super() for a remount.
    The device is taken from the mounted superblock, so fc->source cannot be
    raced to target another device, and fc->sb_flags is vetted alongside the
    option string. On a new mount the forced options must also be present,
    so a filesystem that skipped inserting them has its mount refused rather
    than silently losing them.
    
    The parse-time ve_devmnt_process() call is kept as a best-effort early
    reject, so a disallowed device or option is refused before the
    filesystem's fill_super() runs.
    
    Fix a bug where a containers that use the legacy mount(2) syscall are able
    to reconfigure a mount and change the superblock flags, for example
    from RO to RW. Compute the effective superblock flags and emit rw incase
    SB_RDONLY is missing in vfs_format_sb_flags.
    
    Explicitly deny mounts for device backed legacy filesystems. So even
    if FS_VIRTUALISED is added to them, mounting will not succeed since
    legacy filesystems don't go through vfs_parse_monolithic_sep and the
    policy is not applied to them. Keep this check until all filesystems
    have migrated to the new mount API.
    
    https://virtuozzo.atlassian.net/browse/VSTOR-132330
    Feature: ve: ve generic structures
    Fixes: 263467c864c5 ("ve/fs/devmnt: process mount options")
    Signed-off-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
    Co-developed-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
    Signed-off-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
    Reviewed-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
---
 fs/fs_context.c            | 193 +++++++++++++++++++++++++++++++++++++++++++++
 fs/internal.h              |   8 ++
 fs/namespace.c             | 110 ++++++++++++++++++++------
 fs/super.c                 |  12 +++
 include/linux/fs_context.h |   2 +
 include/linux/mount.h      |   2 +
 6 files changed, 304 insertions(+), 23 deletions(-)

diff --git a/fs/fs_context.c b/fs/fs_context.c
index 76f34f3d468ea..b2bd21a420836 100644
--- a/fs/fs_context.c
+++ b/fs/fs_context.c
@@ -81,6 +81,70 @@ static int vfs_parse_sb_flag(struct fs_context *fc, const char *key)
 	return -ENOPARAM;
 }
 
+/*
+ * Emit option @name into @buff at *@off, prefixed with ',' if the buffer
+ * already holds text. Advances *@off. Returns 0 or -E2BIG if @buff is full.
+ */
+static int __vfs_emit_flag(const char *name, char *buff,
+			   size_t size, size_t *off)
+{
+	ssize_t ret;
+
+	if (*off) {
+		if (*off + 1 >= size)
+			return -E2BIG;
+		buff[(*off)++] = ',';
+	}
+
+	ret = strscpy(buff + *off, name, size - *off);
+	if (ret < 0)
+		return -E2BIG;
+	*off += ret;
+	return 0;
+}
+
+static int __vfs_format_flags(const struct constant_table *p, unsigned int flags,
+			      char *buff, size_t size, size_t *off)
+{
+	for (; p->name; p++) {
+		int ret;
+
+		if (!(flags & p->value))
+			continue;
+		ret = __vfs_emit_flag(p->name, buff, size, off);
+		if (ret)
+			return ret;
+	}
+	return 0;
+}
+
+static int vfs_format_sb_flags(char *buff, size_t size, size_t *off,
+			       unsigned int sb_flags)
+{
+	int err;
+
+	err = __vfs_format_flags(common_set_sb_flag, sb_flags, buff, size, off);
+	if (err)
+		return err;
+
+	/*
+	 * "rw" has no flag bit of its own - it is simply the absence of
+	 * SB_RDONLY. Emit it explicitly so the ve_devmnt policy can allow or
+	 * deny read-write access as a first-class option; otherwise a mount or
+	 * remount that leaves the superblock read-write carries no token and
+	 * slips past the "every option must be allowed" check.
+	 *
+	 * @sb_flags is the effective post-operation flag word, so this reflects
+	 * the state the superblock actually ends up in. A remount that only
+	 * touches an unrelated flag (e.g. "sync") keeps its current SB_RDONLY
+	 * and so does not emit "rw".
+	 */
+	if (!(sb_flags & SB_RDONLY))
+		return __vfs_emit_flag("rw", buff, size, off);
+
+	return 0;
+}
+
 /**
  * vfs_parse_fs_param_source - Handle setting "source" via parameter
  * @fc: The filesystem context to modify
@@ -224,6 +288,119 @@ static inline int fscontext_lookup_bdev(struct fs_context *fc, dev_t *s_dev)
 	return -ENODEV;
 }
 
+/*
+ * ve_devmnt_deny_legacy - refuse a device mount that escapes the policy
+ * @fc: the mount context
+ *
+ * A legacy context, one for a filesystem with no ->init_fs_context passes
+ * its mount data straight to ->mount()/->remount_fs() through
+ * legacy_parse_monolithic(), so vfs_parse_monolithic_sep() never runs on it.
+ * The container's forced options are then never inserted and fc->ve_final_opts
+ * stays empty, which would leave ve_devmnt_verify_fc() vetting an empty option
+ * string while the filesystem acts on the full one userspace passed.
+ *
+ * Returns 0 if the mount may go ahead, -EPERM if it must not.
+ */
+static int ve_devmnt_deny_legacy(struct fs_context *fc)
+{
+	struct ve_struct *ve = get_exec_env();
+
+	if (fc->ops != &legacy_fs_context_ops)
+		return 0;
+
+	ve_pr_warn_ratelimited(VE_LOG_BOTH,
+			       "VE%s: refusing to mount %s: filesystem has no fs_context support\n",
+			       ve_name(ve), fc->fs_type->name);
+	return -EPERM;
+}
+
+/*
+ * ve_devmnt_verify_fc - check a mount against the container device-mount policy
+ * @fc: the mount context, with fc->root set
+ * @new_mount: true at vfs_get_tree() (new mount), false at reconfigure_super()
+ *
+ * Vets the stashed userspace option string plus the synthesized SB_* flag
+ * names against the mounted superblock's device. A device absent from the
+ * policy that is mounted with no userspace options is allowed. Returns 0 when
+ * permitted (or no check applies), or a negative errno.
+ */
+int ve_devmnt_verify_fc(struct fs_context *fc, bool new_mount)
+{
+	struct ve_struct *ve = get_exec_env();
+	unsigned int sb_flags;
+	bool have_user_opts;
+	size_t off = 0;
+	char *page;
+	int err;
+
+	if (ve_is_super(ve))
+		return 0;
+
+	if (!fc->fs_type || !(fc->fs_type->fs_flags & FS_REQUIRES_DEV))
+		return 0;
+
+	/*
+	 * Keep this check until all filesystems have migrated to the new
+	 * mount API
+	 */
+	err = ve_devmnt_deny_legacy(fc);
+	if (err)
+		return err;
+
+	/*
+	 * Filesystems with binary mount data (e.g. btrfs) bypass option
+	 * string parsing entirely, so our checks cannot apply here.
+	 */
+	if (fc->fs_type->fs_flags & FS_BINARY_MOUNTDATA)
+		return 0;
+
+	if (WARN_ON_ONCE(!fc->root))
+		return -EINVAL;
+
+	page = (char *)__get_free_page(GFP_KERNEL);
+	if (!page)
+		return -ENOMEM;
+
+	/*
+	 * Track whether userspace actually supplied options. @page below also
+	 * gets the synthesized ro/rw flag token, so its length cannot answer
+	 * this; ve_final_opts holds only the userspace string.
+	 */
+	have_user_opts = fc->ve_final_opts && *fc->ve_final_opts;
+	if (have_user_opts) {
+		ssize_t ret = strscpy(page, fc->ve_final_opts, PAGE_SIZE);
+
+		if (ret < 0) {
+			err = -E2BIG;
+			goto out;
+		}
+		off = ret;
+	}
+
+	/*
+	 * On a remount fc->sb_flags holds only the bits being changed, so
+	 * combine them with the current superblock flags to get the state the
+	 * sb will actually have - the same value reconfigure_super() writes
+	 * back. On a new mount fc->sb_flags is already the full flag word.
+	 */
+	sb_flags = fc->sb_flags;
+	if (!new_mount)
+		sb_flags = (fc->root->d_sb->s_flags & ~fc->sb_flags_mask) |
+			   (fc->sb_flags & fc->sb_flags_mask);
+
+	err = vfs_format_sb_flags(page, PAGE_SIZE, &off, sb_flags);
+	if (err)
+		goto out;
+
+	page[off] = '\0';
+	err = ve_devmnt_verify(ve, fc->root->d_sb->s_dev, page, new_mount,
+			       have_user_opts);
+
+out:
+	free_page((unsigned long)page);
+	return err;
+}
+
 static int fscontext_init_lazy_opts(struct fs_context *fc)
 {
 	struct ve_struct *ve = get_exec_env();
@@ -389,10 +566,22 @@ int vfs_parse_monolithic_sep(struct fs_context *fc, void *data,
 			return -ENODEV;
 		}
 
+		/* Early reject and hidden-option insertion; verified for real later. */
 		ret = ve_devmnt_process(ve, bd_dev, (void **) &options,
 				fc->purpose == FS_CONTEXT_FOR_RECONFIGURE);
 		if (ret)
 			return ret;
+
+		/* Stash what the filesystem parses; checked in the common mount path. */
+		if (options) {
+			kfree(fc->ve_final_opts);
+			fc->ve_final_opts = kstrdup(options, GFP_KERNEL);
+			if (!fc->ve_final_opts) {
+				if (options != options_orig)
+					free_page((unsigned long)options);
+				return -ENOMEM;
+			}
+		}
 	}
 
 	/*
@@ -614,6 +803,7 @@ struct fs_context *vfs_dup_fs_context(struct fs_context *src_fc)
 	fc->s_fs_info	= NULL;
 	fc->source	= NULL;
 	fc->security	= NULL;
+	fc->ve_final_opts = NULL;
 	get_filesystem(fc->fs_type);
 	get_net(fc->net_ns);
 	get_user_ns(fc->user_ns);
@@ -742,6 +932,7 @@ void put_fs_context(struct fs_context *fc)
 	put_filesystem(fc->fs_type);
 	if (fc->lazy_opts)
 		free_page((unsigned long)fc->lazy_opts);
+	kfree(fc->ve_final_opts);
 	kfree(fc->source);
 	kfree(fc);
 }
@@ -962,6 +1153,8 @@ void vfs_clean_context(struct fs_context *fc)
 		free_page((unsigned long)fc->lazy_opts);
 		fc->lazy_opts = NULL;
 	}
+	kfree(fc->ve_final_opts);
+	fc->ve_final_opts = NULL;
 	kfree(fc->source);
 	fc->source = NULL;
 	fc->exclusive = false;
diff --git a/fs/internal.h b/fs/internal.h
index 3647ce69b2c7a..f1892959db48e 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -46,6 +46,14 @@ extern void __init chrdev_init(void);
  */
 extern const struct fs_context_operations legacy_fs_context_ops;
 extern int parse_monolithic_mount_data(struct fs_context *, void *);
+#ifdef CONFIG_VE
+extern int ve_devmnt_verify_fc(struct fs_context *fc, bool new_mount);
+#else
+static inline int ve_devmnt_verify_fc(struct fs_context *fc, bool new_mount)
+{
+	return 0;
+}
+#endif
 extern void vfs_clean_context(struct fs_context *fc);
 extern int finish_clean_context(struct fs_context *fc);
 
diff --git a/fs/namespace.c b/fs/namespace.c
index 43493f779c592..4d4dc52903508 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -3258,6 +3258,92 @@ int ve_devmnt_process(struct ve_struct *ve, dev_t dev, void **data_pp, int remou
 	return err;
 }
 
+/* Return 0 if every option in @options is listed in @a or @b, else -EPERM. */
+static int ve_devmnt_options_subset(char *options, char *a, char *b)
+{
+	char *copy, *cur, *p;
+	int err = 0;
+
+	if (!options || !*options)
+		return 0;
+	if (!a && !b)
+		return -EPERM;
+
+	copy = cur = kstrdup(options, GFP_KERNEL);
+	if (!copy)
+		return -ENOMEM;
+
+	while ((p = strsep(&cur, ",")) != NULL) {
+		if (!*p)
+			continue;
+		if ((!a || !strstr_separated(a, p, ',')) &&
+		    (!b || !strstr_separated(b, p, ','))) {
+			err = -EPERM;
+			break;
+		}
+	}
+
+	kfree(copy);
+	return err;
+}
+
+/*
+ * ve_devmnt_verify - enforce the container device-mount policy for @dev
+ * @ve: the container
+ * @dev: device taken from the mounted superblock (not from a raceable path)
+ * @opts: mount options plus the SB_* flag names to vet
+ * @new_mount: true for a new mount, false for a remount
+ * @have_user_opts: true if userspace supplied any mount options. @opts always
+ *	carries the kernel-synthesized ro/rw flag token, so it is never empty
+ *	and cannot answer this on its own.
+ *
+ * Every supplied option must be allowed or forced. On a new mount the forced
+ * ("hidden") options must also be present: a filesystem that skipped inserting
+ * them is refused rather than silently dropping a container's mandated option
+ */
+int ve_devmnt_verify(struct ve_struct *ve, dev_t dev, char *opts, bool new_mount,
+		     bool have_user_opts)
+{
+	struct ve_devmnt *devmnt;
+	char *allowed = NULL, *hidden = NULL;
+	bool found = false;
+	int err = 0;
+
+	if (ve->is_pseudosuper)
+		return 0;
+
+	mutex_lock(&ve->devmnt_mutex);
+	list_for_each_entry(devmnt, &ve->devmnt_list, link) {
+		if (devmnt->dev == dev) {
+			allowed = devmnt->allowed_options;
+			hidden = devmnt->hidden_options;
+			found = true;
+			break;
+		}
+	}
+
+	/*
+	 * Enforce for a listed device, or for any mount carrying userspace
+	 * options. An unlisted device with no userspace options is unconstrained
+	 * here, so the synthesized ro/rw token in @opts does not deny it.
+	 */
+	if (found || have_user_opts) {
+		/* every supplied option must be either allowed or forced */
+		err = ve_devmnt_options_subset(opts, allowed, hidden);
+
+		/* on a new mount every forced option must have reached the fs */
+		if (!err && new_mount)
+			err = ve_devmnt_options_subset(hidden, opts, NULL);
+	}
+	mutex_unlock(&ve->devmnt_mutex);
+
+	if (err == -EPERM)
+		ve_pr_warn_ratelimited(VE_LOG_BOTH,
+				       "VE%s: mount options not permitted for device %u:%u\n",
+				       ve_name(ve), MAJOR(dev), MINOR(dev));
+	return err;
+}
+
 static inline int ve_mount_allowed(void)
 {
 	struct ve_struct *ve = get_exec_env();
@@ -3308,23 +3394,6 @@ static inline void ve_mount_nr_inc(struct mount *mnt, struct ve_struct *ve) { }
 static inline void ve_mount_nr_dec(struct mount *mnt) { }
 #endif /* CONFIG_VE */
 
-static int ve_prepare_mount_options(struct fs_context *fc, void *data)
-{
-#ifdef CONFIG_VE
-	struct super_block *sb = fc->root->d_sb;
-	struct ve_struct *ve = get_exec_env();
-
-	if (sb->s_bdev && data && !ve_is_super(ve)) {
-		int err;
-
-		err = ve_devmnt_process(ve, sb->s_bdev->bd_dev, &data, 1);
-		if (err)
-			return err;
-	}
-#endif
-	return 0;
-}
-
 /*
  * change filesystem flags. dir should be a physical root of filesystem.
  * If you've mounted a non-root directory somewhere and want to do remount
@@ -3357,12 +3426,6 @@ static int do_remount(struct path *path, int ms_flags, int sb_flags,
 	 */
 	fc->oldapi = true;
 
-	err = ve_prepare_mount_options(fc, data);
-	if (err) {
-		put_fs_context(fc);
-		return err;
-	}
-
 	err = parse_monolithic_mount_data(fc, data);
 	if (!err) {
 		down_write(&sb->s_umount);
@@ -3816,6 +3879,7 @@ static int do_new_mount(struct path *path, const char *fstype, int sb_flags,
 					  subtype, strlen(subtype));
 	if (!err && name)
 		err = vfs_parse_fs_string(fc, "source", name, strlen(name));
+	/* Container device-mount policy is enforced later, in vfs_get_tree(). */
 	if (!err)
 		err = parse_monolithic_mount_data(fc, data);
 	if (!err && !mount_capable(fc))
diff --git a/fs/super.c b/fs/super.c
index 1adebbf358032..c0c067eb2d8e1 100644
--- a/fs/super.c
+++ b/fs/super.c
@@ -1085,6 +1085,11 @@ int reconfigure_super(struct fs_context *fc)
 	if (retval)
 		return retval;
 
+	/* Enforce the container device-mount policy on the remount options. */
+	retval = ve_devmnt_verify_fc(fc, false);
+	if (retval)
+		return retval;
+
 	if (fc->sb_flags_mask & SB_RDONLY) {
 #ifdef CONFIG_BLOCK
 		if (!(fc->sb_flags & SB_RDONLY) && sb->s_bdev &&
@@ -1924,6 +1929,13 @@ int vfs_get_tree(struct fs_context *fc)
 		return error;
 	}
 
+	/* Enforce the container device-mount policy against the real device. */
+	error = ve_devmnt_verify_fc(fc, true);
+	if (unlikely(error)) {
+		fc_drop_locked(fc);
+		return error;
+	}
+
 	/*
 	 * filesystems should never set s_maxbytes larger than MAX_LFS_FILESIZE
 	 * but s_maxbytes was an unsigned long long for many releases. Throw
diff --git a/include/linux/fs_context.h b/include/linux/fs_context.h
index 1801aed1da67c..2ca586e2cc2a2 100644
--- a/include/linux/fs_context.h
+++ b/include/linux/fs_context.h
@@ -93,6 +93,8 @@ struct fs_context {
 	struct file_system_type	*fs_type;
 	void			*fs_private;	/* The filesystem's context */
 	void			*lazy_opts;	/* mount options which can't be checked at fsconfig() time */
+	/* option string handed to the fs, for the ve_devmnt policy check */
+	char			*ve_final_opts;
 	void			*sget_key;
 	struct dentry		*root;		/* The root and superblock */
 	struct user_namespace	*user_ns;	/* The user namespace for this mount */
diff --git a/include/linux/mount.h b/include/linux/mount.h
index 0cbc6f6893c01..ab0ea4f7afc6f 100644
--- a/include/linux/mount.h
+++ b/include/linux/mount.h
@@ -127,5 +127,7 @@ extern int cifs_root_data(char **dev, char **opts);
 
 struct ve_struct;
 extern int ve_devmnt_process(struct ve_struct *, dev_t, void **, int);
+extern int ve_devmnt_verify(struct ve_struct *ve, dev_t dev, char *opts,
+			    bool new_mount, bool have_user_opts);
 
 #endif /* _LINUX_MOUNT_H */

^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH VZ10 v5 7/9] ve: Introduce per-VE failcount
  2026-08-02 11:40  8% ` [Devel] [PATCH VZ10 v5 7/9] ve: Introduce per-VE failcount Vladimir Riabchun
@ 2026-08-07  9:25  0%   ` Vasileios Almpanis
  0 siblings, 0 replies; 200+ results
From: Vasileios Almpanis @ 2026-08-07  9:25 UTC (permalink / raw)



On 8/2/26 1:40 PM, 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       |  6 ++++
>   kernel/bpf/syscall.c     |  1 +
>   kernel/ve/ve.c           | 67 ++++++++++++++++++++++++++++++++++++++++
>   net/core/dev.c           |  2 ++
>   net/core/neighbour.c     |  1 +
>   net/core/net_namespace.c |  4 ++-
>   8 files changed, 83 insertions(+), 1 deletion(-)
>
> diff --git a/fs/aio.c b/fs/aio.c
> index cb63416af135..3fa07cc626f8 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) {
> +		atomic_inc(&ve->aio_failcount);
>   		spin_unlock(&ve->aio_nr_lock);
>   		err = -EAGAIN;
>   		goto err_ctx;
> diff --git a/fs/namespace.c b/fs/namespace.c
> index 2550aeba2f1e..c4e2c7f7f725 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
> @@ -3283,6 +3283,8 @@ static inline int ve_try_reserve_mount(struct ve_struct *ve)
>   
>   	if (ret)
>   		get_ve(ve);
> +	else
> +		atomic_inc(&ve->mnt_failcount);
>   	return ret;
>   }
>   
> diff --git a/include/linux/ve.h b/include/linux/ve.h
> index 5687faad46ff..9e73527e970e 100644
> --- a/include/linux/ve.h
> +++ b/include/linux/ve.h
> @@ -72,12 +72,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;
> @@ -86,6 +89,7 @@ struct ve_struct {
>   
>   	atomic_t		arp_neigh_nr;
>   	atomic_t		nd_neigh_nr;
> +	atomic_t		neigh_tbl_failcount;
>   	unsigned long		meminfo_val;
>   
>   	/*
> @@ -94,6 +98,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
> @@ -121,6 +126,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;
>   };
> diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
> index ff2a51c59f04..95e806fa19f4 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) {
> +			atomic_inc(&load_ve->bpf_prog_failcount);
>   			load_ve = NULL;
>   			err = -ENOSPC;
>   			goto put_token;
> diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
> index dc3faa0b1d76..a5c93be759ac 100644
> --- a/kernel/ve/ve.c
> +++ b/kernel/ve/ve.c
> @@ -99,10 +99,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	=
> @@ -114,8 +117,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,
>   	.umh_running_helpers	= ATOMIC_INIT(0),
>   	.umh_helpers_waitq	= __WAIT_QUEUE_HEAD_INITIALIZER(ve0.umh_helpers_waitq),
> @@ -780,12 +791,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);
> @@ -812,7 +826,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
> @@ -825,6 +841,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;
> @@ -1066,6 +1083,50 @@ 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));
> +	struct ve_failcount_entry *entry;
> +	atomic_t *fc;
> +
kernel/ve/ve.c: In function ?ve_failcount_read?:
kernel/ve/ve.c:1107:20: warning: assignment discards ?const? qualifier 
from pointer target type [-Wdiscarded-qualifiers]
 ?1107 |? ? ? ? ?for (entry = ve_failcounts; entry->name; entry++) {
 ? ? ? |? ? ? ? ? ? ? ? ? ? ^
kernel/ve/ve.c: In function ?ve_failcount_write?:
kernel/ve/ve.c:1121:20: warning: assignment discards ?const? qualifier 
from pointer target type [-Wdiscarded-qualifiers]
 ?1121 |? ? ? ? ?for (entry = ve_failcounts; entry->name; entry++) {

the left-hand pointee must carry all qualifiers of the right-hand pointee according to C11


> +	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));
> +	struct ve_failcount_entry *entry;
> +	atomic_t *fc;
> +
> +	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);
> @@ -1603,6 +1664,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..05e0b9b6ba23 100644
> --- a/net/core/dev.c
> +++ b/net/core/dev.c
> @@ -10997,6 +10997,7 @@ int register_netdevice(struct net_device *dev)
>   
>   	ret = -ENOMEM;
>   	if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
> +		atomic_inc(&net->owner_ve->netif_failcount);
>   		ve_pr_warn_ratelimited(VE_LOG_BOTH,
>   			"CT%s: hits max number of network devices, "
>   			"increase ve::netif_max_nr parameter\n",
> @@ -12211,6 +12212,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) {
> +		atomic_inc(&net->owner_ve->netif_failcount);
>   		ve_pr_warn_ratelimited(VE_LOG_BOTH,
>   			"CT%s: hits max number of network devices, "
>   			"increase ve::netif_max_nr parameter\n",
> diff --git a/net/core/neighbour.c b/net/core/neighbour.c
> index f90deb17fb25..57a49d9c98a7 100644
> --- a/net/core/neighbour.c
> +++ b/net/core/neighbour.c
> @@ -520,6 +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) {
> +			atomic_inc(&ve->neigh_tbl_failcount);
>   			net_info_ratelimited("%s: neighbor table overflow!\n",
>   					     tbl->id);
>   			NEIGH_CACHE_STAT_INC(tbl, table_fulls);
> diff --git a/net/core/net_namespace.c b/net/core/net_namespace.c
> index b3d54cad984a..9a3376d2682f 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) {
> +		atomic_inc(&ve->netns_failcount);
>   		return -ENOSPC;
> +	}
>   	return 0;
>   }
>   

-- 
Best regards, Vasileios Almpanis
Software Developer, Virtuozzo.


^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH VZ10 v5 9/9] selftests/ve: Add mount accounting selftest
  2026-08-02 11:40  5% ` [Devel] [PATCH VZ10 v5 9/9] selftests/ve: Add mount accounting selftest Vladimir Riabchun
@ 2026-08-07 10:01  0%   ` Vasileios Almpanis
  0 siblings, 0 replies; 200+ results
From: Vasileios Almpanis @ 2026-08-07 10:01 UTC (permalink / raw)



On 8/2/26 1:40 PM, 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. Patial mounts test, when mount limit is hit in the middle
>     of creation.
nit: Partial
> 4. Test that enabled pseudosuper allows overuse.
> 5. Test that pseudosuper doesn't affect mount accoutning.
nit: 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>
> ---
> v4 -> v5:
>   - Splitted pseudosuper test into two.
>   - Added new test to check that things go smoothly when
>     we run out of mounts in the middle of a new mount.
>
>   tools/testing/selftests/ve/.gitignore         |   1 +
>   tools/testing/selftests/ve/Makefile           |   1 +
>   .../selftests/ve/ve_mount_accounting_test.c   | 419 ++++++++++++++++++
>   3 files changed, 421 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..b295290ec6e8
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_mount_accounting_test.c
> @@ -0,0 +1,419 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * ve_mount_accounting selftests
> + *
> + * Tests to check the correctness of mount accounting.
> + */
> +#define _GNU_SOURCE
> +#include <linux/sched.h>
> +#include <linux/mount.h>
Is linux/mount needed here? You include both linux/mount and sys/mount.
You only call mount,umount so dropping it looks generally safe.
> +#include <sched.h>
> +#include <sys/wait.h>
> +#include <sys/syscall.h>
> +#include <unistd.h>
> +#include <asm/unistd.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <fcntl.h>
> +#include <string.h>
> +#include <sys/stat.h>
> +#include <sys/mount.h>
> +#include <linux/limits.h>
> +#include <errno.h>
> +
> +#include "../kselftest_harness.h"
> +#include "ve_selftest.h"
> +
> +#define TMP_DIR			"/ve-mnt-tmp/"
> +#define VE_MOUNTS_MAX		128
> +
> +static int set_pseudosuper(int cgv2_fd, int ctid, int value)
> +{
> +	char path[64];
> +
> +	snprintf(path, sizeof(path), "%d/ve.pseudosuper", ctid);
> +	return write_u64_at(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 ret;
> +}
> +
> +static int create_mount(int cgv2_fd, int ctid, int id)
> +{
> +	int ret = run_in_ve(cgv2_fd, ctid, CLONE_NEWVE, _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(int cgv2_fd, int ctid, int id)
> +{
> +	int ret = run_in_ve(cgv2_fd, ctid, CLONE_NEWVE, _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(int cgv2_fd, int ctid)
> +{
> +	int avail1, avail2, mnt_id;
> +	char path[64];
> +
> +	mnt_id = get_free_mnt_id();
> +
> +	snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", ctid);
> +	if (mnt_id < 0 ||
> +	    read_s32_at(cgv2_fd, path, &avail1) ||
> +	    create_mount(cgv2_fd, ctid, mnt_id) ||
> +	    read_s32_at(cgv2_fd, path, &avail2) ||
> +	    destroy_mount(cgv2_fd, ctid, mnt_id))
> +		return -1;
> +
> +	return avail1 - avail2;
> +}
> +
> +/* Expect mount success and return new avail value */
> +static int mount_and_get_avail(struct __test_metadata *_metadata,
> +			int cgv2_fd, int ctid, int mnt_id)
> +{
> +	char path_avail[64];
> +	int mnt_avail_nr;
> +
> +	snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", ctid);
> +
> +	ASSERT_EQ(create_mount(cgv2_fd, ctid, mnt_id), 0);
> +	ASSERT_EQ(read_s32_at(cgv2_fd, path_avail, &mnt_avail_nr), 0);
> +	return mnt_avail_nr;
> +}
> +
> +/* Expect mount failure and ensure intact avail number */
> +static void assert_mount_fails(struct __test_metadata *_metadata,
> +			int cgv2_fd, int ctid, 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", ctid);
> +
> +	ASSERT_EQ(read_s32_at(cgv2_fd, path_avail, &mnt_avail_nr), 0);
> +	ASSERT_EQ(mnt_avail_nr, avail_count);
> +	ASSERT_LT(create_mount(cgv2_fd, ctid, mnt_id), 0);
> +	ASSERT_EQ(read_s32_at(cgv2_fd, path_avail, &mnt_avail_nr), 0);
> +	ASSERT_EQ(mnt_avail_nr, avail_count);
> +}
> +
> +FIXTURE(ve_mnt_acc)
> +{
> +	int cgv2_fd;
> +	int ctid;
> +};
> +
> +FIXTURE_SETUP(ve_mnt_acc)
> +{
> +	unsigned long long 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_u64_at(self->cgv2_fd, path, &initial_mnt_avail_nr), 0);
> +	ASSERT_EQ(initial_mnt_avail_nr, VE_MOUNTS_MAX);
> +};
> +
> +FIXTURE_TEARDOWN(ve_mnt_acc)
> +{
Since we mount tmpfs on host mount namespace (we dont pass CLONE_NEWNS), 
should we iterate here and umount all the ids that remain mounted after 
tests bail? There are maybe places where create_mount is tried and if 
assertion fails the mount remains and leaks to the host possibly also 
pinning the ve namespace since in ve_try_reserve_mount we get a refcount 
on it.

> +	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->cgv2_fd, self->ctid, 0),
> +		  original_mnt_avail);
> +
> +	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 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 original_mnt_avail, mnt_avail_nr, mnt_cost;
> +	int original_have_mnt;
> +	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);
> +
> +	mnt_cost = get_mount_cost(self->cgv2_fd, self->ctid);
> +	ASSERT_GE(mnt_cost, 1);
> +
> +	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &original_mnt_avail), 0);
> +	original_have_mnt = VE_MOUNTS_MAX - original_mnt_avail;
> +
> +	/* Step 1: reduce number of available mounts to mnt_cost */
> +	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, original_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 2: do one mount, no mounts should be available */
> +	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 0),
> +		  0);
> +
> +	/* Step 3: check that one more mount falils */
> +	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 1, 0);
> +
> +	/* Step 4: increase mount limit a little bit, mount should still fail */
> +	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
> +				original_have_mnt + 2 * mnt_cost - 1), 0);
> +	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 1, mnt_cost - 1);
> +
> +	/* Step 5: increase by 1 and win now */
> +	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, original_have_mnt + 2 * mnt_cost), 0);
> +	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
> +	ASSERT_EQ(mnt_avail_nr, mnt_cost);
> +
> +	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 1),
> +		  0);
> +
> +	/* Step 6: reduce mnt_max_nr so we have more mounts than allowed */
> +	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, original_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 7: try to do mount when avail < 0, ensure number is intact */
> +	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, -1 * mnt_cost);
> +
> +	/* Step 8: remove one mount, check avail value update, mount should fail */
> +	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 0), 0);
> +	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, 0);
> +
> +	/* Step 9: remove one more mount and check that new mount succeeds */
> +	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 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(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 2),
> +		  0);
> +
> +	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 2), 0);
> +}
> +
> +/*
> + * Mount propagation makes one mount cost more.
> + * This test check that if we run out or 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 mount_cost, i, orig_have, orig_mnt_avail;
> +
> +	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);
> +
> +	mount_cost = get_mount_cost(self->cgv2_fd, self->ctid);
> +	ASSERT_GE(mount_cost, 1);
> +	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &orig_mnt_avail), 0);
> +	orig_have = VE_MOUNTS_MAX - orig_mnt_avail;
> +
> +	if (mount_cost == 1)
> +		SKIP(return, "mount cost is 1, no partial mounts possible");
> +
> +	for (i = 0; i < mount_cost; i++) {
> +		ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, orig_have + i), 0);
> +		assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 0, i);
> +	}
> +}
> +
> +/* Test that pseudosuper allows negative avail with correct accounting. */
> +TEST_F(ve_mnt_acc, pseudosuper_allows_overuse)
> +{
> +	int orig_mnt_avail, orig_have;
> +	int mount_cost;
> +	char path_avail[64], path_max_nr[64];
> +
> +	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);
> +	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &orig_mnt_avail), 0);
> +	orig_have = VE_MOUNTS_MAX - orig_mnt_avail;
> +
> +	mount_cost = get_mount_cost(self->cgv2_fd, self->ctid);
> +	ASSERT_GE(mount_cost, 1);
> +
> +	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, orig_have + mount_cost), 0);
> +	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 1), 0);
> +
> +	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 0),
> +		  0);
> +	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 1),
> +		  -1 * mount_cost);
> +
> +	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 0), 0);
> +
> +	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, -1 * mount_cost);
> +	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 0), 0);
> +	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, 0);
> +	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 1), 0);
> +	ASSERT_EQ(get_mount_cost(self->cgv2_fd, self->ctid), mount_cost);
> +}
> +
> +/* Test that pseudosuper doesn't disable accounting. */
> +TEST_F(ve_mnt_acc, pseudosuper_continues_accounting)
> +{
> +	int orig_mnt_avail, mount_cost, 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);
> +	mount_cost = get_mount_cost(self->cgv2_fd, self->ctid);
> +	ASSERT_GE(mount_cost, 1);
> +
> +	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 0), 0);
> +
> +	/* mnt 0 - mounted without pseudosuper, umounted with it. */
> +	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 0),
> +		  orig_mnt_avail - mount_cost);
> +	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 1), 0);
> +
> +	/* Cost is the same when mount/umount happen under pseudosuper. */
> +	ASSERT_EQ(get_mount_cost(self->cgv2_fd, self->ctid), mount_cost);
> +
> +	/* mnt 1 - mounted with pseudosuper, umounted without it. */
> +	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 1),
> +		  orig_mnt_avail - 2 * mount_cost);
> +
> +	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 0), 0);
> +	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
> +	ASSERT_EQ(mnt_avail_nr, orig_mnt_avail - mount_cost);
> +
> +	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 0), 0);
> +
> +	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 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->cgv2_fd, self->ctid, 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->cgv2_fd, self->ctid, 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->cgv2_fd, self->ctid, 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->cgv2_fd, self->ctid, 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);
> +}
> +
> +TEST_HARNESS_MAIN

-- 
Best regards, Vasileios Almpanis
Software Developer, Virtuozzo.


^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH vz10] selftests: build test modules against the kernel tree
  @ 2026-08-10 16:29  3% ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-10 16:29 UTC (permalink / raw)


1. the patch should go along with changes in kernel.spec i believe because at the moment "livepatch" is not even in the TARGETS list in kerne.spec

2. before committing this please show me 
* the PR with successfully built those .ko files
* and list of files which are included now in the kselftests rpm
* and the successful run of the kselftests - logs of all those 3 TARGETS, how did they run.

3. Please note https://virtuozzo.atlassian.net/browse/VSTOR-139647?focusedCommentId=3980596
when you'll update kernel.spec.

On 8/9/26 20:18, Eva Kurchatova wrote:
> Selftests livepatch, cgroup and mm/page_frag build out-of-tree kernel
> modules and default KDIR to /lib/modules/$(uname -r)/build, which is
> the running kernel build tree on the currently building host.
> That is wrong whenever the selftests are built as part of a kernel
> package: `uname -r` is not the kernel being packaged, so the modules
> either are not built at all (no source tree for the builders kernel)
> or come out with the wrong vermagic and symbol layout.
> Either way no usable test modules end up in the packaged test suite,
> and a testing host may not have the kernel build tree to compile them
> either, so the tests will fail.
> 
> Default KDIR to the kernel tree the tests are built from, or to its O=
> build directory, whenever that tree is configured and built.
> This is exactly what bpf/test_kmods has been already doing all along,
> and its modules are packaged and loaded successfully today.
> Testing for include/config/auto.conf and Module.symvers keeps the previous
> behaviour for a bare source checkout, and for a source tree that was
> mrpropered after the kernel had been built (packaging copies
> Module.symvers back into such a tree), so nothing that builds today
> starts failing.
> 
> Also test for $(KDIR)/Makefile rather than for $(KDIR) itself before
> descending into the kernel tree: /lib/modules/$(uname -r)/build is
> commonly a dangling symlink, which the previous test accepted before
> failing in make -C. Same protection is applied to bpf/page_frag, as it
> had no such check at all.
> 
> With this, `make kselftest TARGETS=livepatch` in a built kernel tree
> builds the modules for that kernel and `make install` ships them, so an
> installed testsuite runs against pre-built modules and needs no kernel
> build tree at all.
> 
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-139647
> Feature: fix selftests
> ---
>  .../selftests/cgroup/test_modules/Makefile    | 25 ++++++++++++++++---
>  .../selftests/livepatch/test_modules/Makefile | 25 ++++++++++++++++---
>  tools/testing/selftests/mm/Makefile           | 14 +++++++++++
>  tools/testing/selftests/mm/page_frag/Makefile | 22 ++++++++++++++++
>  4 files changed, 78 insertions(+), 8 deletions(-)
> 
> diff --git a/tools/testing/selftests/cgroup/test_modules/Makefile b/tools/testing/selftests/cgroup/test_modules/Makefile
> index 3f39eeda3a92..a47af9b7f18a 100644
> --- a/tools/testing/selftests/cgroup/test_modules/Makefile
> +++ b/tools/testing/selftests/cgroup/test_modules/Makefile
> @@ -1,17 +1,34 @@
>  TESTMODS_DIR := $(realpath $(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
> +
> +# Kernel build tree to compile the test modules against. Prefer the
> +# kernel tree these tests are part of (or its O= build directory) when it
> +# is configured and built, like bpf/test_kmods already does: When the

s/When/when/

> +# selftests are built as part of a kernel package, uname -r is the build
> +# host's kernel and not the kernel being packaged, so the resulting
> +# modules would carry the wrong vermagic and symbol layout. Fall back to
> +# the running kernel's build tree for standalone builds.
> +KDIR_TREE := $(if $(O),$(abspath $(O)),$(abspath $(TESTMODS_DIR)/../../../../..))
> +ifneq (,$(wildcard $(KDIR_TREE)/include/config/auto.conf))
> +ifneq (,$(wildcard $(KDIR_TREE)/Module.symvers))
> +KDIR ?= $(KDIR_TREE)
> +endif
> +endif
>  KDIR ?= /lib/modules/$(shell uname -r)/build
>  
>  obj-m += cg_freezer_hang.o \
>  	cg_freezer_kthread.o
>  
> -# Ensure that KDIR exists, otherwise skip the compilation
> +# Ensure that KDIR is a kernel build tree, otherwise skip the compilation.
> +# Testing for the Makefile rather than for the directory itself also covers
> +# a dangling /lib/modules/$(uname -r)/build symlink, and an installed
> +# testsuite, which ships pre-built modules and has no kernel tree above it.
>  modules:
> -ifneq ("$(wildcard $(KDIR))", "")
> +ifneq ("$(wildcard $(KDIR)/Makefile)", "")
>  	$(Q)$(MAKE) -C $(KDIR) modules KBUILD_EXTMOD=$(TESTMODS_DIR)
>  endif
>  
> -# Ensure that KDIR exists, otherwise skip the clean target
> +# Ensure that KDIR is a kernel build tree, otherwise skip the clean target
>  clean:
> -ifneq ("$(wildcard $(KDIR))", "")
> +ifneq ("$(wildcard $(KDIR)/Makefile)", "")
>  	$(Q)$(MAKE) -C $(KDIR) clean KBUILD_EXTMOD=$(TESTMODS_DIR)
>  endif
> diff --git a/tools/testing/selftests/livepatch/test_modules/Makefile b/tools/testing/selftests/livepatch/test_modules/Makefile
> index 939230e571f5..7dc026fd42f7 100644
> --- a/tools/testing/selftests/livepatch/test_modules/Makefile
> +++ b/tools/testing/selftests/livepatch/test_modules/Makefile
> @@ -1,4 +1,18 @@
>  TESTMODS_DIR := $(realpath $(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
> +
> +# Kernel build tree to compile the test modules against. Prefer the
> +# kernel tree these tests are part of (or its O= build directory) when it
> +# is configured and built, like bpf/test_kmods already does: when the
> +# selftests are built as part of a kernel package, uname -r is the build
> +# host's kernel and not the kernel being packaged, so the resulting
> +# modules would carry the wrong vermagic and symbol CRCs. Fall back to
> +# the running kernel's build tree for standalone builds.
> +KDIR_TREE := $(if $(O),$(abspath $(O)),$(abspath $(TESTMODS_DIR)/../../../../..))
> +ifneq (,$(wildcard $(KDIR_TREE)/include/config/auto.conf))
> +ifneq (,$(wildcard $(KDIR_TREE)/Module.symvers))
> +KDIR ?= $(KDIR_TREE)
> +endif
> +endif
>  KDIR ?= /lib/modules/$(shell uname -r)/build

i do not think we need to fix this right now, but just for the record:
if we are in <kernel tree>/selftests/livepatch and call
  make -C livepatch O=../../../../out3

the compilation will fail (because of the relative path in O=)

>  
>  obj-m += test_klp_atomic_replace.o \
> @@ -14,14 +28,17 @@ obj-m += test_klp_atomic_replace.o \
>  	test_klp_state3.o \
>  	test_klp_syscall.o
>  
> -# Ensure that KDIR exists, otherwise skip the compilation
> +# Ensure that KDIR is a kernel build tree, otherwise skip the compilation.
> +# Testing for the Makefile rather than for the directory itself also covers
> +# a dangling /lib/modules/$(uname -r)/build symlink, and an installed
> +# testsuite, which ships pre-built modules and has no kernel tree above it.
>  modules:
> -ifneq ("$(wildcard $(KDIR))", "")
> +ifneq ("$(wildcard $(KDIR)/Makefile)", "")
>  	$(Q)$(MAKE) -C $(KDIR) modules KBUILD_EXTMOD=$(TESTMODS_DIR)
>  endif
>  
> -# Ensure that KDIR exists, otherwise skip the clean target
> +# Ensure that KDIR is a kernel build tree, otherwise skip the clean target
>  clean:
> -ifneq ("$(wildcard $(KDIR))", "")
> +ifneq ("$(wildcard $(KDIR)/Makefile)", "")
>  	$(Q)$(MAKE) -C $(KDIR) clean KBUILD_EXTMOD=$(TESTMODS_DIR)
>  endif
> diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
> index 3de23ea4663f..3e643834d3a2 100644
> --- a/tools/testing/selftests/mm/Makefile
> +++ b/tools/testing/selftests/mm/Makefile
> @@ -36,6 +36,20 @@ MAKEFLAGS += --no-builtin-rules
>  CFLAGS = -Wall -I $(top_srcdir) $(EXTRA_CFLAGS) $(KHDR_INCLUDES) $(TOOLS_INCLUDES)
>  LDLIBS = -lrt -lpthread -lm
>  
> +# Kernel build tree to compile page_frag_test.ko against. Prefer the
> +# kernel tree these tests are part of (or its O= build directory) when it
> +# is configured and built, like bpf/test_kmods already does: when the
> +# selftests are built as part of a kernel package, uname -r is the build
> +# host's kernel and not the kernel being packaged, so the resulting module
> +# would carry the wrong vermagic and symbol CRCs. Fall back to the
> +# running kernel's build tree for standalone builds. Keep this in sync
> +# with page_frag/Makefile, which is invoked separately by lib.mk.
> +KDIR_TREE := $(if $(O),$(abspath $(O)),$(abspath $(CURDIR)/../../../..))
> +ifneq (,$(wildcard $(KDIR_TREE)/include/config/auto.conf))
> +ifneq (,$(wildcard $(KDIR_TREE)/Module.symvers))
> +KDIR ?= $(KDIR_TREE)
> +endif
> +endif
>  KDIR ?= /lib/modules/$(shell uname -r)/build
>  ifneq (,$(wildcard $(KDIR)/Module.symvers))
>  ifneq (,$(wildcard $(KDIR)/include/linux/page_frag_cache.h))
> diff --git a/tools/testing/selftests/mm/page_frag/Makefile b/tools/testing/selftests/mm/page_frag/Makefile
> index 8c8bb39ffa28..2c2918bbe0eb 100644
> --- a/tools/testing/selftests/mm/page_frag/Makefile
> +++ b/tools/testing/selftests/mm/page_frag/Makefile
> @@ -1,4 +1,18 @@
>  PAGE_FRAG_TEST_DIR := $(realpath $(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
> +
> +# Kernel build tree to compile the test module against. Prefer the kernel
> +# tree this test is part of (or its O= build directory) when it is
> +# configured and built, like bpf/test_kmods already does: when the
> +# selftests are built as part of a kernel package, uname -r is the build
> +# host's kernel and not the kernel being packaged, so the resulting module
> +# would carry the wrong vermagic and symbol CRCs. Fall back to the
> +# running kernel's build tree for standalone builds.
> +KDIR_TREE := $(if $(O),$(abspath $(O)),$(abspath $(PAGE_FRAG_TEST_DIR)/../../../../..))
> +ifneq (,$(wildcard $(KDIR_TREE)/include/config/auto.conf))
> +ifneq (,$(wildcard $(KDIR_TREE)/Module.symvers))
> +KDIR ?= $(KDIR_TREE)
> +endif
> +endif
>  KDIR ?= /lib/modules/$(shell uname -r)/build

  make O=/tmp/vz10-obj -j$(nproc)       # to generate include/config/auto.conf and Module.symvers
  make O=/tmp/vz10-obj kselftest-all TARGETS=mm

You will see:
  Warning: missing page_frag_cache.h, please use a newer kernel. page_frag test will be skipped.


  The O= kernel build breaks, and only for one module - page_frag_test.ko. The other three Makefiles from the patch
  (livepatch, cgroup, page_frag/Makefile) handle O= correctly.

  In tools/testing/selftests/mm/Makefile, right below the new block, there is an old check the patch did not touch:

  KDIR ?= /lib/modules/$(shell uname -r)/build
  ifneq (,$(wildcard $(KDIR)/Module.symvers))
  ifneq (,$(wildcard $(KDIR)/include/linux/page_frag_cache.h))
  TEST_GEN_MODS_DIR := page_frag
  else
  PAGE_FRAG_WARNING = "missing page_frag_cache.h, please use a newer kernel"

  It looks for the source header include/linux/page_frag_cache.h inside KDIR. After the patch, in an O= build KDIR points at
  the build directory ($(O)), which only contains include/config/ and include/generated/ - the source headers stay in the
  source tree. The first check (Module.symvers) passes, the second one does not.

  Further down the chain: TEST_GEN_MODS_DIR is not set, lib.mk never descends into the page_frag/ directory, the module is
  not built and does not get into make install, and test_page_frag.sh hits if [ ! -f $DRIVER ] at run time and exits with
  the skip code. On top of that, the build prints missing page_frag_cache.h, please use a newer kernel, which points at an
  entirely wrong cause.

  Before the patch, in this scenario KDIR was /lib/modules/$(uname -r)/build, where the header is present, so the module was
  built and loaded on that same machine. In other words, the commit message claim "nothing that builds today starts
  failing" does not hold for this case.

  An in-tree build (no O=) is fine: there both Module.symvers and the header live in the same directory.

  How to fix it

  The minimal and safe option is to also look for the header through the source symlink that outputmakefile creates in the
  build directory (ln -fsn $(srctree) source, present in this tree):

  PAGE_FRAG_HDR := $(firstword $(wildcard \
        $(KDIR)/include/linux/page_frag_cache.h \
        $(KDIR)/source/include/linux/page_frag_cache.h))
  ifneq (,$(PAGE_FRAG_HDR))
  TEST_GEN_MODS_DIR := page_frag

>  
>  ifeq ($(V),1)
> @@ -11,8 +25,16 @@ MODULES = page_frag_test.ko
>  
>  obj-m += page_frag_test.o
>  
> +# Ensure that KDIR is a kernel build tree, otherwise skip the compilation.
> +# Testing for the Makefile rather than for the directory itself also covers
> +# a dangling /lib/modules/$(uname -r)/build symlink, and an installed
> +# testsuite, which ships a pre-built module and has no kernel tree above it.
>  all:
> +ifneq ("$(wildcard $(KDIR)/Makefile)", "")
>  	+$(Q)make -C $(KDIR) M=$(PAGE_FRAG_TEST_DIR) modules
> +endif
>  
>  clean:
> +ifneq ("$(wildcard $(KDIR)/Makefile)", "")
>  	+$(Q)make -C $(KDIR) M=$(PAGE_FRAG_TEST_DIR) clean
> +endif


^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH VZ10] x86/bugs: Make Safe-RET robust against interrupt injection
@ 2026-08-11 11:49 19% Pavel Tikhomirov
  2026-08-11 14:48 19% ` [Devel] [PATCH RHEL10 COMMIT] ms/x86/bugs: " Konstantin Khorenko
  0 siblings, 1 reply; 200+ results
From: Pavel Tikhomirov @ 2026-08-11 11:49 UTC (permalink / raw)


From: "Borislav Petkov (AMD)" <bp@alien8.de>

commit 7e7f81cf6f5ca3311e526308f55d7c54d3ba71f9 upstream.

An attacker injecting interrupts while the Safe-RET mitigation executes
on machines affected by SRSO can neutralize the safe return sequence,
potentially leading to data leakage through speculative execution.

Fixup register state as if the Safe-RET sequence executed successfully
by "emulating" it, in a manner of speaking, and avoid executing a RET
instruction after returning from the interrupt.

Co-developed-by: David Kaplan <David.Kaplan@amd.com>
Signed-off-by: David Kaplan <David.Kaplan@amd.com>
Signed-off-by: Borislav Petkov (AMD) <bp@alien8.de>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>

Also adding pt_regs forward declaration to avoid possible compilation
warnings.

CVE-2026-68480
https://virtuozzo.atlassian.net/browse/VSTOR-140991
(cherry picked from commit bfe7f9993467ba431b2731437949ac1e2634e771)
Signed-off-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
---
 arch/x86/entry/entry_64.S            |  8 +++-
 arch/x86/include/asm/nospec-branch.h | 59 ++++++++++++++++++++++++++++
 arch/x86/kernel/cpu/bugs.c           | 39 ++++++++++++++++++
 arch/x86/lib/retpoline.S             | 20 ++++++++++
 4 files changed, 125 insertions(+), 1 deletion(-)

diff --git a/arch/x86/entry/entry_64.S b/arch/x86/entry/entry_64.S
index b4cd6ddde9747..a14e425554d8c 100644
--- a/arch/x86/entry/entry_64.S
+++ b/arch/x86/entry/entry_64.S
@@ -935,6 +935,8 @@ SYM_CODE_START(paranoid_entry)
 	IBRS_ENTER save_reg=%r15
 	UNTRAIN_RET_FROM_CALL
 
+	HANDLE_INTR_SAFERET 8(%rsp)
+
 	RET
 SYM_CODE_END(paranoid_entry)
 
@@ -1037,6 +1039,11 @@ SYM_CODE_START(error_entry)
 	movl	%ecx, %eax			/* zero extend */
 	cmpq	%rax, RIP+8(%rsp)
 	je	.Lbstep_iret
+
+	VALIDATE_UNRET_END
+
+	HANDLE_INTR_SAFERET 8(%rsp)
+
 	cmpq	$.Lgs_change, RIP+8(%rsp)
 	jne	.Lerror_entry_done_lfence
 
@@ -1055,7 +1062,6 @@ SYM_CODE_START(error_entry)
 	FENCE_SWAPGS_KERNEL_ENTRY
 	CALL_DEPTH_ACCOUNT
 	leaq	8(%rsp), %rax			/* return pt_regs pointer */
-	VALIDATE_UNRET_END
 	RET
 
 .Lbstep_iret:
diff --git a/arch/x86/include/asm/nospec-branch.h b/arch/x86/include/asm/nospec-branch.h
index deb105c07de92..7fb9ad9597d20 100644
--- a/arch/x86/include/asm/nospec-branch.h
+++ b/arch/x86/include/asm/nospec-branch.h
@@ -13,6 +13,7 @@
 #include <asm/unwind_hints.h>
 #include <asm/percpu.h>
 #include <asm/current.h>
+#include <asm/ptrace-abi.h>
 
 /*
  * Call depth tracking for Intel SKL CPUs to address the RSB underflow
@@ -177,6 +178,50 @@
 	add	$(BITS_PER_LONG/8), %_ASM_SP;		\
 	lfence;
 
+/*
+ * Helper for detecting if an interrupt occurred at an unsafe location within
+ * Safe-RET.  If Safe-RET is interrupted after the CALL or LEA the RSB may get
+ * poisoned by the interrupt handler.
+ *
+ * The Safe-RET sequence is:
+ *
+ * CALL
+ * LEA 8(%RSP), %RSP
+ * RET
+ *
+ * The two CMPs below check whether RIP points to after the CALL or after the
+ * LEA.
+ *
+ * The LFENCE below is to address this particular speculation case:
+ *
+ * 1. Userspace runs and poisons the BTB around the safe-RET routine
+ *
+ * 2. Userspace triggers some kind of exception
+ *
+ * 3. Kernel executes error_entry() and mis-speculates the branch into thinking
+ *    it actually came from kernel space
+ *
+ * 4. The kernel then further mis-speculates that the exception occurred due
+ *    to an interrupted safe-RET
+ *
+ * 5. The handle_interrupted_saferet() routine speculatively executes and
+ *    speculatively does a safe-RET. But this is unsafe since it was never
+ *    untrained.
+ *
+ * The LFENCE fixes this by ensuring step 5 is never reached speculatively.
+ * Note that this LFENCE only occurs if safe-RET was actually interrupted (so
+ * it's outside of the normal path).
+ */
+#define __HANDLE_INTR_SAFERET(name, pt_regs)		\
+	cmpq	$(name), RIP+pt_regs;			\
+	jb	1f;					\
+	cmpq	$(name)+5, RIP+pt_regs;			\
+	ja	1f;					\
+	lfence;						\
+	leaq	pt_regs, %rdi;				\
+	call	handle_interrupted_saferet;		\
+	1:
+
 #ifdef __ASSEMBLY__
 
 /*
@@ -295,6 +340,14 @@
 #define UNTRAIN_RET_FROM_CALL \
 	__UNTRAIN_RET X86_FEATURE_ENTRY_IBPB, __stringify(RESET_CALL_DEPTH_FROM_CALL)
 
+.macro HANDLE_INTR_SAFERET pt_regs
+#ifdef CONFIG_MITIGATION_SRSO
+	ALTERNATIVE_2 "", \
+	__stringify(__HANDLE_INTR_SAFERET(srso_safe_ret, \pt_regs)), X86_FEATURE_SRSO, \
+	__stringify(__HANDLE_INTR_SAFERET(srso_alias_safe_ret, \pt_regs)), X86_FEATURE_SRSO_ALIAS
+
+#endif
+.endm
 
 .macro CALL_DEPTH_ACCOUNT
 #ifdef CONFIG_MITIGATION_CALL_DEPTH_TRACKING
@@ -618,6 +671,12 @@ static __always_inline void x86_idle_clear_cpu_buffers(void)
 		x86_clear_cpu_buffers();
 }
 
+struct pt_regs;
+
+void srso_safe_ret(void);
+void srso_alias_safe_ret(void);
+void handle_interrupted_saferet(struct pt_regs *regs);
+
 #endif /* __ASSEMBLY__ */
 
 #endif /* _ASM_X86_NOSPEC_BRANCH_H_ */
diff --git a/arch/x86/kernel/cpu/bugs.c b/arch/x86/kernel/cpu/bugs.c
index b8daba654d85f..c722fcc1cd627 100644
--- a/arch/x86/kernel/cpu/bugs.c
+++ b/arch/x86/kernel/cpu/bugs.c
@@ -3794,3 +3794,42 @@ void __warn_thunk(void)
 {
 	WARN_ONCE(1, "Unpatched return thunk in use. This should not happen!\n");
 }
+
+#ifdef CONFIG_MITIGATION_SRSO
+/*
+ * Called during exception/interrupt entry if interrupted during the
+ * safe-RET sequence.  The safe-RET sequence consists of 3 instructions:
+ *
+ *	CALL
+ *	LEA 8(%RSP), %RSP
+ *	RET
+ *
+ * An interrupt after the CALL or after the LEA could potentially lead
+ * to branch predictor poisoning and results in the sequence not being
+ * able to be safely resumed.
+ *
+ * Therefore, modify the regs state as if the remaining part of the
+ * safe-RET sequence executed so the interrupt returns back to the
+ * desired return target, instead of the to the safe-RET sequence.
+ */
+void noinstr handle_interrupted_saferet(struct pt_regs *regs)
+{
+	unsigned long rip = regs->ip;
+
+	if (rip == (unsigned long) srso_safe_ret ||
+	    rip == (unsigned long) srso_alias_safe_ret) {
+	    /* Modify stack pointer as if LEA executed: */
+	    regs->sp += 8;
+	}
+
+	/*
+	 * Adjust registers as if RET executed:
+	 *
+	 * 1. Read the return address off the stack and into rIP:
+	 */
+	regs->ip = *(unsigned long *)(regs->sp);
+
+	/* 2. Pop rIP off the stack: */
+	regs->sp += 8;
+}
+#endif /* CONFIG_MITIGATION_SRSO */
diff --git a/arch/x86/lib/retpoline.S b/arch/x86/lib/retpoline.S
index 614fb9aee2ff6..bc66ce29ccc8b 100644
--- a/arch/x86/lib/retpoline.S
+++ b/arch/x86/lib/retpoline.S
@@ -168,10 +168,24 @@ __EXPORT_THUNK(srso_alias_untrain_ret)
 
 	.pushsection .text..__x86.rethunk_safe
 SYM_CODE_START_NOALIGN(srso_alias_safe_ret)
+
+	/*
+	 * Tell objtool that those are not function pointers referenced by
+	 * __HANDLE_INTR_SAFERET(). Below too.
+	 */
+	ANNOTATE_NOENDBR
+
+	/*
+	 * Safe-RET sequence. If you need to change it, adjust
+	 * handle_interrupted_saferet() too.
+	 */
 	lea 8(%_ASM_SP), %_ASM_SP
 	UNWIND_HINT_FUNC
+
+	ANNOTATE_NOENDBR
 	ANNOTATE_UNRET_SAFE
 	ret
+	/* End of Safe-RET sequence */
 	int3
 SYM_FUNC_END(srso_alias_safe_ret)
 
@@ -206,8 +220,14 @@ SYM_CODE_START_LOCAL_NOALIGN(srso_untrain_ret)
  * the stack.
  */
 SYM_INNER_LABEL(srso_safe_ret, SYM_L_GLOBAL)
+	/*
+	 * Safe-RET sequence. If you need to change it, adjust
+	 * handle_interrupted_saferet() too.
+	 */
 	lea 8(%_ASM_SP), %_ASM_SP
 	ret
+	/* End of Safe-RET sequence */
+
 	int3
 	int3
 	/* end of movabs */
-- 
2.55.0


^ permalink raw reply	[relevance 19%]

* [Devel] [PATCH RHEL10 COMMIT] ms/x86/bugs: Make Safe-RET robust against interrupt injection
  2026-08-11 11:49 19% [Devel] [PATCH VZ10] x86/bugs: Make Safe-RET robust against interrupt injection Pavel Tikhomirov
@ 2026-08-11 14:48 19% ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-11 14:48 UTC (permalink / raw)


The commit is pushed to "branch-rh10-6.12.0-211.39.1.16.x.vz10-ovz" and will appear at git at bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.39.1.16.3.vz10
------>
commit 7c5f71404ed2e471b416cab264e8cb9ecb9845d5
Author: Borislav Petkov (AMD) <bp@alien8.de>
Date:   Tue Aug 11 13:49:35 2026 +0200

    ms/x86/bugs: Make Safe-RET robust against interrupt injection
    
    commit 7e7f81cf6f5ca3311e526308f55d7c54d3ba71f9 upstream.
    
    An attacker injecting interrupts while the Safe-RET mitigation executes
    on machines affected by SRSO can neutralize the safe return sequence,
    potentially leading to data leakage through speculative execution.
    
    Fixup register state as if the Safe-RET sequence executed successfully
    by "emulating" it, in a manner of speaking, and avoid executing a RET
    instruction after returning from the interrupt.
    
    Co-developed-by: David Kaplan <David.Kaplan@amd.com>
    Signed-off-by: David Kaplan <David.Kaplan@amd.com>
    Signed-off-by: Borislav Petkov (AMD) <bp@alien8.de>
    Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
    
    Also adding pt_regs forward declaration to avoid possible compilation
    warnings.
    
    CVE-2026-68480
    Feature: fix ms/x86
    https://virtuozzo.atlassian.net/browse/VSTOR-140991
    (cherry picked from commit 7e7f81cf6f5ca3311e526308f55d7c54d3ba71f9)
    Signed-off-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
---
 arch/x86/entry/entry_64.S            |  8 ++++-
 arch/x86/include/asm/nospec-branch.h | 59 ++++++++++++++++++++++++++++++++++++
 arch/x86/kernel/cpu/bugs.c           | 39 ++++++++++++++++++++++++
 arch/x86/lib/retpoline.S             | 20 ++++++++++++
 4 files changed, 125 insertions(+), 1 deletion(-)

diff --git a/arch/x86/entry/entry_64.S b/arch/x86/entry/entry_64.S
index b4cd6ddde9747..a14e425554d8c 100644
--- a/arch/x86/entry/entry_64.S
+++ b/arch/x86/entry/entry_64.S
@@ -935,6 +935,8 @@ SYM_CODE_START(paranoid_entry)
 	IBRS_ENTER save_reg=%r15
 	UNTRAIN_RET_FROM_CALL
 
+	HANDLE_INTR_SAFERET 8(%rsp)
+
 	RET
 SYM_CODE_END(paranoid_entry)
 
@@ -1037,6 +1039,11 @@ SYM_CODE_START(error_entry)
 	movl	%ecx, %eax			/* zero extend */
 	cmpq	%rax, RIP+8(%rsp)
 	je	.Lbstep_iret
+
+	VALIDATE_UNRET_END
+
+	HANDLE_INTR_SAFERET 8(%rsp)
+
 	cmpq	$.Lgs_change, RIP+8(%rsp)
 	jne	.Lerror_entry_done_lfence
 
@@ -1055,7 +1062,6 @@ SYM_CODE_START(error_entry)
 	FENCE_SWAPGS_KERNEL_ENTRY
 	CALL_DEPTH_ACCOUNT
 	leaq	8(%rsp), %rax			/* return pt_regs pointer */
-	VALIDATE_UNRET_END
 	RET
 
 .Lbstep_iret:
diff --git a/arch/x86/include/asm/nospec-branch.h b/arch/x86/include/asm/nospec-branch.h
index deb105c07de92..7fb9ad9597d20 100644
--- a/arch/x86/include/asm/nospec-branch.h
+++ b/arch/x86/include/asm/nospec-branch.h
@@ -13,6 +13,7 @@
 #include <asm/unwind_hints.h>
 #include <asm/percpu.h>
 #include <asm/current.h>
+#include <asm/ptrace-abi.h>
 
 /*
  * Call depth tracking for Intel SKL CPUs to address the RSB underflow
@@ -177,6 +178,50 @@
 	add	$(BITS_PER_LONG/8), %_ASM_SP;		\
 	lfence;
 
+/*
+ * Helper for detecting if an interrupt occurred at an unsafe location within
+ * Safe-RET.  If Safe-RET is interrupted after the CALL or LEA the RSB may get
+ * poisoned by the interrupt handler.
+ *
+ * The Safe-RET sequence is:
+ *
+ * CALL
+ * LEA 8(%RSP), %RSP
+ * RET
+ *
+ * The two CMPs below check whether RIP points to after the CALL or after the
+ * LEA.
+ *
+ * The LFENCE below is to address this particular speculation case:
+ *
+ * 1. Userspace runs and poisons the BTB around the safe-RET routine
+ *
+ * 2. Userspace triggers some kind of exception
+ *
+ * 3. Kernel executes error_entry() and mis-speculates the branch into thinking
+ *    it actually came from kernel space
+ *
+ * 4. The kernel then further mis-speculates that the exception occurred due
+ *    to an interrupted safe-RET
+ *
+ * 5. The handle_interrupted_saferet() routine speculatively executes and
+ *    speculatively does a safe-RET. But this is unsafe since it was never
+ *    untrained.
+ *
+ * The LFENCE fixes this by ensuring step 5 is never reached speculatively.
+ * Note that this LFENCE only occurs if safe-RET was actually interrupted (so
+ * it's outside of the normal path).
+ */
+#define __HANDLE_INTR_SAFERET(name, pt_regs)		\
+	cmpq	$(name), RIP+pt_regs;			\
+	jb	1f;					\
+	cmpq	$(name)+5, RIP+pt_regs;			\
+	ja	1f;					\
+	lfence;						\
+	leaq	pt_regs, %rdi;				\
+	call	handle_interrupted_saferet;		\
+	1:
+
 #ifdef __ASSEMBLY__
 
 /*
@@ -295,6 +340,14 @@
 #define UNTRAIN_RET_FROM_CALL \
 	__UNTRAIN_RET X86_FEATURE_ENTRY_IBPB, __stringify(RESET_CALL_DEPTH_FROM_CALL)
 
+.macro HANDLE_INTR_SAFERET pt_regs
+#ifdef CONFIG_MITIGATION_SRSO
+	ALTERNATIVE_2 "", \
+	__stringify(__HANDLE_INTR_SAFERET(srso_safe_ret, \pt_regs)), X86_FEATURE_SRSO, \
+	__stringify(__HANDLE_INTR_SAFERET(srso_alias_safe_ret, \pt_regs)), X86_FEATURE_SRSO_ALIAS
+
+#endif
+.endm
 
 .macro CALL_DEPTH_ACCOUNT
 #ifdef CONFIG_MITIGATION_CALL_DEPTH_TRACKING
@@ -618,6 +671,12 @@ static __always_inline void x86_idle_clear_cpu_buffers(void)
 		x86_clear_cpu_buffers();
 }
 
+struct pt_regs;
+
+void srso_safe_ret(void);
+void srso_alias_safe_ret(void);
+void handle_interrupted_saferet(struct pt_regs *regs);
+
 #endif /* __ASSEMBLY__ */
 
 #endif /* _ASM_X86_NOSPEC_BRANCH_H_ */
diff --git a/arch/x86/kernel/cpu/bugs.c b/arch/x86/kernel/cpu/bugs.c
index b8daba654d85f..c722fcc1cd627 100644
--- a/arch/x86/kernel/cpu/bugs.c
+++ b/arch/x86/kernel/cpu/bugs.c
@@ -3794,3 +3794,42 @@ void __warn_thunk(void)
 {
 	WARN_ONCE(1, "Unpatched return thunk in use. This should not happen!\n");
 }
+
+#ifdef CONFIG_MITIGATION_SRSO
+/*
+ * Called during exception/interrupt entry if interrupted during the
+ * safe-RET sequence.  The safe-RET sequence consists of 3 instructions:
+ *
+ *	CALL
+ *	LEA 8(%RSP), %RSP
+ *	RET
+ *
+ * An interrupt after the CALL or after the LEA could potentially lead
+ * to branch predictor poisoning and results in the sequence not being
+ * able to be safely resumed.
+ *
+ * Therefore, modify the regs state as if the remaining part of the
+ * safe-RET sequence executed so the interrupt returns back to the
+ * desired return target, instead of the to the safe-RET sequence.
+ */
+void noinstr handle_interrupted_saferet(struct pt_regs *regs)
+{
+	unsigned long rip = regs->ip;
+
+	if (rip == (unsigned long) srso_safe_ret ||
+	    rip == (unsigned long) srso_alias_safe_ret) {
+	    /* Modify stack pointer as if LEA executed: */
+	    regs->sp += 8;
+	}
+
+	/*
+	 * Adjust registers as if RET executed:
+	 *
+	 * 1. Read the return address off the stack and into rIP:
+	 */
+	regs->ip = *(unsigned long *)(regs->sp);
+
+	/* 2. Pop rIP off the stack: */
+	regs->sp += 8;
+}
+#endif /* CONFIG_MITIGATION_SRSO */
diff --git a/arch/x86/lib/retpoline.S b/arch/x86/lib/retpoline.S
index 614fb9aee2ff6..bc66ce29ccc8b 100644
--- a/arch/x86/lib/retpoline.S
+++ b/arch/x86/lib/retpoline.S
@@ -168,10 +168,24 @@ SYM_FUNC_END(srso_alias_untrain_ret)
 
 	.pushsection .text..__x86.rethunk_safe
 SYM_CODE_START_NOALIGN(srso_alias_safe_ret)
+
+	/*
+	 * Tell objtool that those are not function pointers referenced by
+	 * __HANDLE_INTR_SAFERET(). Below too.
+	 */
+	ANNOTATE_NOENDBR
+
+	/*
+	 * Safe-RET sequence. If you need to change it, adjust
+	 * handle_interrupted_saferet() too.
+	 */
 	lea 8(%_ASM_SP), %_ASM_SP
 	UNWIND_HINT_FUNC
+
+	ANNOTATE_NOENDBR
 	ANNOTATE_UNRET_SAFE
 	ret
+	/* End of Safe-RET sequence */
 	int3
 SYM_FUNC_END(srso_alias_safe_ret)
 
@@ -206,8 +220,14 @@ /*
  * the stack.
  */
 SYM_INNER_LABEL(srso_safe_ret, SYM_L_GLOBAL)
+	/*
+	 * Safe-RET sequence. If you need to change it, adjust
+	 * handle_interrupted_saferet() too.
+	 */
 	lea 8(%_ASM_SP), %_ASM_SP
 	ret
+	/* End of Safe-RET sequence */
+
 	int3
 	int3
 	/* end of movabs */

^ permalink raw reply	[relevance 19%]

* [Devel] [PATCH DRAFT vz10 1/5] Revert "ve/net/gre: Disable ERSPAN support in ip_gre module"
  @ 2026-08-12 13:03  3% ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-12 13:03 UTC (permalink / raw)


This reverts commit a6adc8063402a38c6d951113453df6bf8ddcfb0e.

ERSPAN was disabled under CONFIG_VE because at the time it was not wired
into the per-Container GRE infrastructure: erspan devices did not set
NETIF_F_VIRTUAL, so register_netdevice() would reject them inside a CT,
and erspan_net_ops was not gated by any VE feature bit. Rather than
carry the feature permanently disabled, bring the code back so that the
following commit can properly containerize ERSPAN under a dedicated
VE feature bit.

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

Feature: net: disable ERSPAN support in ip_gre module
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 net/ipv4/ip_gre.c | 33 ++++-----------------------------
 1 file changed, 4 insertions(+), 29 deletions(-)

diff --git a/net/ipv4/ip_gre.c b/net/ipv4/ip_gre.c
index f618aae250334..d776eb8d9f76c 100644
--- a/net/ipv4/ip_gre.c
+++ b/net/ipv4/ip_gre.c
@@ -1134,7 +1134,6 @@ static int ipgre_tap_validate(struct nlattr *tb[], struct nlattr *data[],
 	return ipgre_tunnel_validate(tb, data, extack);
 }
 
-#ifndef CONFIG_VE
 static int erspan_validate(struct nlattr *tb[], struct nlattr *data[],
 			   struct netlink_ext_ack *extack)
 {
@@ -1174,7 +1173,6 @@ static int erspan_validate(struct nlattr *tb[], struct nlattr *data[],
 
 	return 0;
 }
-#endif
 
 static int ipgre_netlink_parms(struct net_device *dev,
 				struct nlattr *data[],
@@ -1245,7 +1243,6 @@ static int ipgre_netlink_parms(struct net_device *dev,
 	return 0;
 }
 
-#ifndef CONFIG_VE
 static int erspan_netlink_parms(struct net_device *dev,
 				struct nlattr *data[],
 				struct nlattr *tb[],
@@ -1289,7 +1286,6 @@ static int erspan_netlink_parms(struct net_device *dev,
 
 	return 0;
 }
-#endif
 
 /* This function returns true when ENCAP attributes are present in the nl msg */
 static bool ipgre_netlink_encap_parms(struct nlattr *data[],
@@ -1428,7 +1424,6 @@ static int ipgre_newlink(struct net *src_net, struct net_device *dev,
 	return ip_tunnel_newlink(dev, tb, &p, fwmark);
 }
 
-#ifndef CONFIG_VE
 static int erspan_newlink(struct net *src_net, struct net_device *dev,
 			  struct nlattr *tb[], struct nlattr *data[],
 			  struct netlink_ext_ack *extack)
@@ -1446,7 +1441,6 @@ static int erspan_newlink(struct net *src_net, struct net_device *dev,
 		return err;
 	return ip_tunnel_newlink(dev, tb, &p, fwmark);
 }
-#endif
 
 static int ipgre_changelink(struct net_device *dev, struct nlattr *tb[],
 			    struct nlattr *data[],
@@ -1477,7 +1471,6 @@ static int ipgre_changelink(struct net_device *dev, struct nlattr *tb[],
 	return 0;
 }
 
-#ifndef CONFIG_VE
 static int erspan_changelink(struct net_device *dev, struct nlattr *tb[],
 			     struct nlattr *data[],
 			     struct netlink_ext_ack *extack)
@@ -1504,7 +1497,6 @@ static int erspan_changelink(struct net_device *dev, struct nlattr *tb[],
 
 	return 0;
 }
-#endif
 
 static size_t ipgre_get_size(const struct net_device *dev)
 {
@@ -1602,7 +1594,6 @@ static int ipgre_fill_info(struct sk_buff *skb, const struct net_device *dev)
 	return -EMSGSIZE;
 }
 
-#ifndef CONFIG_VE
 static int erspan_fill_info(struct sk_buff *skb, const struct net_device *dev)
 {
 	struct ip_tunnel *t = netdev_priv(dev);
@@ -1643,7 +1634,6 @@ static void erspan_setup(struct net_device *dev)
 	ip_tunnel_setup(dev, erspan_net_id);
 	t->erspan_ver = 1;
 }
-#endif
 
 static const struct nla_policy ipgre_policy[IFLA_GRE_MAX + 1] = {
 	[IFLA_GRE_LINK]		= { .type = NLA_U32 },
@@ -1699,7 +1689,6 @@ static struct rtnl_link_ops ipgre_tap_ops __read_mostly = {
 	.get_link_net	= ip_tunnel_get_link_net,
 };
 
-#ifndef CONFIG_VE
 static struct rtnl_link_ops erspan_link_ops __read_mostly = {
 	.kind		= "erspan",
 	.maxtype	= IFLA_GRE_MAX,
@@ -1714,7 +1703,6 @@ static struct rtnl_link_ops erspan_link_ops __read_mostly = {
 	.fill_info	= erspan_fill_info,
 	.get_link_net	= ip_tunnel_get_link_net,
 };
-#endif
 
 struct net_device *gretap_fb_dev_create(struct net *net, const char *name,
 					u8 name_assign_type)
@@ -1786,7 +1774,6 @@ static struct pernet_operations ipgre_tap_net_ops = {
 	.size = sizeof(struct ip_tunnel_net),
 };
 
-#ifndef CONFIG_VE
 static int __net_init erspan_init_net(struct net *net)
 {
 	return ip_tunnel_init_net(net, erspan_net_id,
@@ -1806,7 +1793,6 @@ static struct pernet_operations erspan_net_ops = {
 	.id   = &erspan_net_id,
 	.size = sizeof(struct ip_tunnel_net),
 };
-#endif
 
 static int __init ipgre_init(void)
 {
@@ -1821,11 +1807,11 @@ static int __init ipgre_init(void)
 	err = register_pernet_device(&ipgre_tap_net_ops);
 	if (err < 0)
 		goto pnet_tap_failed;
-#ifndef CONFIG_VE
+
 	err = register_pernet_device(&erspan_net_ops);
 	if (err < 0)
 		goto pnet_erspan_failed;
-#endif
+
 	err = gre_add_protocol(&ipgre_protocol, GREPROTO_CISCO);
 	if (err < 0) {
 		pr_info("%s: can't add protocol\n", __func__);
@@ -1839,27 +1825,22 @@ static int __init ipgre_init(void)
 	err = rtnl_link_register(&ipgre_tap_ops);
 	if (err < 0)
 		goto tap_ops_failed;
-#ifndef CONFIG_VE
+
 	err = rtnl_link_register(&erspan_link_ops);
 	if (err < 0)
 		goto erspan_link_failed;
-#endif
 
 	return 0;
 
-#ifndef CONFIG_VE
 erspan_link_failed:
-#endif
 	rtnl_link_unregister(&ipgre_tap_ops);
 tap_ops_failed:
 	rtnl_link_unregister(&ipgre_link_ops);
 rtnl_link_failed:
 	gre_del_protocol(&ipgre_protocol, GREPROTO_CISCO);
 add_proto_failed:
-#ifndef CONFIG_VE
 	unregister_pernet_device(&erspan_net_ops);
 pnet_erspan_failed:
-#endif
 	unregister_pernet_device(&ipgre_tap_net_ops);
 pnet_tap_failed:
 	unregister_pernet_device(&ipgre_net_ops);
@@ -1870,15 +1851,11 @@ static void __exit ipgre_fini(void)
 {
 	rtnl_link_unregister(&ipgre_tap_ops);
 	rtnl_link_unregister(&ipgre_link_ops);
-#ifndef CONFIG_VE
 	rtnl_link_unregister(&erspan_link_ops);
-#endif
 	gre_del_protocol(&ipgre_protocol, GREPROTO_CISCO);
 	unregister_pernet_device(&ipgre_tap_net_ops);
 	unregister_pernet_device(&ipgre_net_ops);
-#ifndef CONFIG_VE
 	unregister_pernet_device(&erspan_net_ops);
-#endif
 }
 
 module_init(ipgre_init);
@@ -1887,9 +1864,7 @@ MODULE_DESCRIPTION("IPv4 GRE tunnels over IP library");
 MODULE_LICENSE("GPL");
 MODULE_ALIAS_RTNL_LINK("gre");
 MODULE_ALIAS_RTNL_LINK("gretap");
-#ifndef CONFIG_VE
 MODULE_ALIAS_RTNL_LINK("erspan");
-MODULE_ALIAS_NETDEV("erspan0");
-#endif
 MODULE_ALIAS_NETDEV("gre0");
 MODULE_ALIAS_NETDEV("gretap0");
+MODULE_ALIAS_NETDEV("erspan0");
-- 
2.43.0


^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH vz10 0/3] ve/fs: make mount ownership follow the mount namespace
@ 2026-08-17  7:16  2% Mirian Shilakadze
  2026-08-17  7:16  4% ` [Devel] [PATCH vz10 1/3] ve/fs: unlink the mount namespace on the copy_mnt_ns() error path Mirian Shilakadze
  2026-08-18 10:40  0% ` [Devel] [PATCH vz10 0/3] ve/fs: make mount ownership follow the mount namespace Vasileios Almpanis
  0 siblings, 2 replies; 200+ results
From: Mirian Shilakadze @ 2026-08-17  7:16 UTC (permalink / raw)


mnt->ve_owner is meant to say which VE a mount belongs to. It is read by
ve_check_trusted_file(), which stops ve0 executing content a container
could have written, and by the per-VE mount accounting behind ve.mnt_nr.

It is wrong in two different ways, and each one alone defeats the trusted
exec check. Fixing the second exposed a third bug, a host panic, which is
patch 1 and the reason this is one series rather than two.

Patch 1 fixes the panic. alloc_mnt_ns() links every namespace onto
all_mntns_list and takes a reference on its owning VE, and both are undone
only in free_mnt_ns(). copy_mnt_ns()'s copy_tree() failure path does not
call it, so it frees a namespace that is still linked and leaks the VE
reference, and the next namespace creation runs list_add_tail() through
the dangling entry. A container at its own sysctl_ve_mount_nr limit
calling unshare(CLONE_NEWNS) reaches that path deterministically and
panics the host. This is VSTOR-141545.

Patch 2 fixes ownership not being updated when a mount moves. ve_owner is
assigned once in ve_mount_nr_inc() from alloc_vfsmnt() and never changes,
so a mount the host hands to a container keeps ve_owner == ve0 while
living in the container's mount namespace, and a host tmpfs bindmounted
into a container is trusted even though the container can write to it.
This is VSTOR-141322.

Patch 3 fixes ownership being wrong at creation. A new mount takes its
owner from get_exec_env(), the VE of the task, rather than the VE of the
mount namespace being worked in. Those differ for a ve0 task that entered
a container's mount namespace with nsenter -m. copy_mnt_ns() and
open_detached_copy() both build mounts that way and neither reaches
commit_tree(), so a plain unshare(CLONE_NEWNS) or an
open_tree(OPEN_TREE_CLONE) hands back ve0 owned copies of container
content. This is VSTOR-141429.

Patches 2 and 3 apply the same rule, the one already used when a namespace
is copied at container creation: ownership comes from the mount namespace,
never from the calling task.

Testing
=======

Built on 6.12.0-211.39.1.16.4.vz10, debug flavour, with KASAN, lockdep,
PROVE_LOCKING, DEBUG_ATOMIC_SLEEP, DEBUG_LIST, DEBUG_VM and gcov on
fs/namespace.o. Also compiled with CONFIG_VE=n. Each commit builds
standalone, so bisect is safe.

Before and after on the same host, same commands. Stock
6.12.0-211.30.1.14.4.vz10 on the left, the series on the right:

  container at its mount limit, unshare(CLONE_NEWNS)  PANIC    -> survives
  CT tmpfs, plain nsenter (control)                   refused  -> refused
  nsenter + unshare -m                                EXECUTED -> refused
  nsenter + open_tree + execveat                      EXECUTED -> refused
  host tmpfs lent via --bindmount_add                 EXECUTED -> refused

The panic was captured on stock over netconsole: "list_add corruption ...
kernel BUG at lib/list_debug.c:32", Comm: unshare, ve: 900, trace
alloc_mnt_ns <- copy_mnt_ns <- unshare. Against the series the identical
sequence, 150 container unshares at the limit followed by 300 host
namespace creations, completes with no corruption and no dump. The
control case behaving the same on both kernels shows the difference is
the change and not the environment.

No KASAN, lockdep, atomic sleep, refcount or list corruption reports
across any of it. DEBUG_ATOMIC_SLEEP staying quiet covers the one thing
worth asking about in patch 2, that get_ve()/put_ve() are called under
lock_mount_hash().

ve.mnt_nr drifted by 0 over 15 bindmount add and remove cycles, and
nr_dying_descendants moved by 1 across 150 opportunities to leak a VE
reference, so patch 1 releases the reference rather than merely not
crashing.

Coverage from gcov, so the new code is known to have run rather than just
linked: ve_mount_reown() called 54300 times with the ownership transfer
branch taken 958 times, and both commit_tree() call sites exercised,
20024 for the moved tree and 24081 for the propagation loop.

selftests: mount 2/2, mount_setattr 21/21, ve_perms 14/14, ve_ns_owner
2/2. The last one matters most, it asserts ve.mnt_nr behaviour around
CLONE_NEWVE, which is the line patch 3 changes.

vzctl functional suite: 538 of 603 passed. About 25 of the failures are
vzctl returning exit 21 where the suite asserts 20 for an unrecognized
option, which never reaches the kernel. The other ten were re-run one at
a time: two passed, and the rest fail in container creation or disk setup
on a test filesystem out of space, not on the mount operations they
exercise.

KCSAN, on a separate build of the same series with KCSAN enabled at
runtime: 2520 reports over a 15 minute run, drained continuously so that
is every report rather than what happened to survive in the ring buffer,
while KCSAN's own counter went from 5174 to 10179 data races. None of
them names ve_owner, commit_tree(), ve_check_trusted_file(),
is_sb_ve_accessible() or mnt_ns_unlink(). The code carrying the new store
was hot and instrumented throughout: attach_recursive_mnt(), which calls
commit_tree(), appears in 201 stack traces, and 193 of the reports are
races in propagate_one() and propagate_mnt() beside it. Those are
pre-existing upstream races on inode and mount fields, not on ve_owner.

Not addressed here
==================

Patch 2 lets a container be pushed above sysctl_ve_mount_nr by mounts the
host gives it, and while over it the container's own mounts are refused
until the count drops. The default limit is 4096 so this takes an unusual
number of lent mounts, and the container can unmount them, but it is the
host's action that spends the container's budget. Enforcing the limit at
handover is possible, attach_recursive_mnt() already does the equivalent
for sysctl_mount_max in count_mounts(), but ve_mount_allowed() tests the
creating task's VE while the counter follows the owner, so the limit
currently has two meanings and picking one is a separate decision.

Mirian Shilakadze (3):
  ve/fs: unlink the mount namespace on the copy_mnt_ns() error path
  ve/fs: transfer mount ownership when a mount enters another VE
  ve/fs: take the owner of copied mounts from the namespace, not the task

 fs/mount.h     |  2 +-
 fs/namespace.c | 73 ++++++++++++++++++++++++++++++++++++++++++++------
 kernel/ve/ve.c |  6 ++++-
 3 files changed, 71 insertions(+), 10 deletions(-)

-- 
2.43.0

^ permalink raw reply	[relevance 2%]

* [Devel] [PATCH vz10 1/3] ve/fs: unlink the mount namespace on the copy_mnt_ns() error path
  2026-08-17  7:16  2% [Devel] [PATCH vz10 0/3] ve/fs: make mount ownership follow the mount namespace Mirian Shilakadze
@ 2026-08-17  7:16  4% ` Mirian Shilakadze
  2026-08-26 15:38  4%   ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
  2026-08-18 10:40  0% ` [Devel] [PATCH vz10 0/3] ve/fs: make mount ownership follow the mount namespace Vasileios Almpanis
  1 sibling, 1 reply; 200+ results
From: Mirian Shilakadze @ 2026-08-17  7:16 UTC (permalink / raw)


alloc_mnt_ns() does two things upstream does not: it links the namespace
onto all_mntns_list and takes a reference on its owning VE. Both are
undone in free_mnt_ns(), which holds the only list_del() of mntns_list
and the only put_ve(ns->ve_owner) in the file.

copy_mnt_ns() does not call it when copy_tree() fails. It open codes the
teardown and finishes with mnt_ns_release(), which drops the passive
count and kfree()s the namespace without unlinking it, so the namespace
is freed while all_mntns_list still points at it and the VE reference is
leaked. The next namespace creation runs list_add_tail() through the
dangling entry.

A container reaches this deterministically. alloc_vfsmnt() returns NULL
when !ve_mount_allowed(), that is when the VE is at sysctl_ve_mount_nr,
clone_mnt() turns that into -ENOMEM and copy_tree() propagates it. So a
container sitting at its own mount limit that calls unshare(CLONE_NEWNS)
takes the error path every time, with no memory pressure and nothing
beyond CAP_SYS_ADMIN in its own user namespace, and panics the host:

  list_add corruption. prev->next should be next (ffffffffa9ca77f0), but
  was ff2834a3cdfaeed0. (prev=ff2834a3cdfaeed0).
  kernel BUG at lib/list_debug.c:32!
  CPU: 94 UID: 0 PID: 7139 Comm: unshare ve: 900
   alloc_mnt_ns+0xd5/0x210
   copy_mnt_ns+0x82/0x3c0
   create_new_namespaces+0x5d/0x2f0
   unshare_nsproxy_namespaces+0x69/0xc0
   ksys_unshare+0x213/0x3f0

prev->next == prev is INIT_LIST_HEAD() on reallocated memory, the freed
namespace reused while the list still referenced it. CONFIG_DEBUG_LIST is
only what makes it a clean BUG, without it the same list_add_tail()
writes through the dangling pointer silently.

The path used to call free_mnt_ns() and was correct. Upstream replaced
that with the open coded sequence because free_mnt_ns() reaches
mnt_ns_tree_remove(), which rb_erase()s a node that copy_mnt_ns() has not
inserted yet, mnt_ns_tree_add() running only after the copy succeeds.
Upstream is unaffected by the replacement because its free_mnt_ns()
carries nothing else. Ours does.

So do not restore the free_mnt_ns() call, that would reintroduce the
rb_erase() upstream fixed. Split the part that is ours into
mnt_ns_unlink() and call it from both places, so a future addition to
namespace teardown has one home rather than two that can drift apart,
which is how this happened.

Fixes: 229fd15908fe ("fs: don't try and remove empty rbtree node")
Fixes: 1db60e545f65 ("ve/mntns: add ve_owner to struct mnt_namespace")
https://virtuozzo.atlassian.net/browse/VSTOR-141545
Feature: ve: ve generic structures
Signed-off-by: Mirian Shilakadze <mirian.shilakadze@virtuozzo.com>
---
 fs/namespace.c | 24 +++++++++++++++++++-----
 1 file changed, 19 insertions(+), 5 deletions(-)

diff --git a/fs/namespace.c b/fs/namespace.c
index 4d4dc5290350..7e27537dcdaf 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -4243,17 +4243,30 @@ static void dec_mnt_namespaces(struct ucounts *ucounts)
 static LIST_HEAD(all_mntns_list);
 static DEFINE_SPINLOCK(all_mntns_list_lock);
 
-static void free_mnt_ns(struct mnt_namespace *ns)
+/*
+ * Undo the bookkeeping alloc_mnt_ns() sets up beyond what upstream does: the
+ * entry on all_mntns_list and the reference on the owning VE.
+ *
+ * Kept separate from free_mnt_ns() because copy_mnt_ns() has to unwind a
+ * namespace that is not in mnt_ns_tree yet, so it cannot use free_mnt_ns()
+ * without rb_erase()ing a node that was never inserted.
+ */
+static void mnt_ns_unlink(struct mnt_namespace *ns)
 {
-	if (!is_anon_ns(ns))
-		ns_free_inum(&ns->ns);
-	dec_mnt_namespaces(ns->ucounts);
-
 	spin_lock(&all_mntns_list_lock);
 	list_del(&ns->mntns_list);
 	spin_unlock(&all_mntns_list_lock);
 
 	put_ve(ns->ve_owner);
+}
+
+static void free_mnt_ns(struct mnt_namespace *ns)
+{
+	if (!is_anon_ns(ns))
+		ns_free_inum(&ns->ns);
+	dec_mnt_namespaces(ns->ucounts);
+
+	mnt_ns_unlink(ns);
 
 	mnt_ns_tree_remove(ns);
 }
@@ -4347,6 +4360,7 @@ struct mnt_namespace *copy_mnt_ns(unsigned long flags, struct mnt_namespace *ns,
 		namespace_unlock();
 		ns_free_inum(&new_ns->ns);
 		dec_mnt_namespaces(new_ns->ucounts);
+		mnt_ns_unlink(new_ns);
 		mnt_ns_release(new_ns);
 		return ERR_CAST(new);
 	}
-- 
2.43.0


^ permalink raw reply	[relevance 4%]

* Re: [Devel] [PATCH vz10 0/3] ve/fs: make mount ownership follow the mount namespace
  2026-08-17  7:16  2% [Devel] [PATCH vz10 0/3] ve/fs: make mount ownership follow the mount namespace Mirian Shilakadze
  2026-08-17  7:16  4% ` [Devel] [PATCH vz10 1/3] ve/fs: unlink the mount namespace on the copy_mnt_ns() error path Mirian Shilakadze
@ 2026-08-18 10:40  0% ` Vasileios Almpanis
  1 sibling, 0 replies; 200+ results
From: Vasileios Almpanis @ 2026-08-18 10:40 UTC (permalink / raw)


> mnt->ve_owner is meant to say which VE a mount belongs to. It is read by
> ve_check_trusted_file(), which stops ve0 executing content a container
> could have written, and by the per-VE mount accounting behind ve.mnt_nr.
> 
> It is wrong in two different ways, and each one alone defeats the trusted
> exec check. Fixing the second exposed a third bug, a host panic, which is
> patch 1 and the reason this is one series rather than two.
> 
> Patch 1 fixes the panic. alloc_mnt_ns() links every namespace onto
> all_mntns_list and takes a reference on its owning VE, and both are undone
> only in free_mnt_ns(). copy_mnt_ns()'s copy_tree() failure path does not
> call it, so it frees a namespace that is still linked and leaks the VE
> reference, and the next namespace creation runs list_add_tail() through
> the dangling entry. A container at its own sysctl_ve_mount_nr limit
> calling unshare(CLONE_NEWNS) reaches that path deterministically and
> panics the host. This is VSTOR-141545.
> 
> Patch 2 fixes ownership not being updated when a mount moves. ve_owner is
> assigned once in ve_mount_nr_inc() from alloc_vfsmnt() and never changes,
> so a mount the host hands to a container keeps ve_owner == ve0 while
> living in the container's mount namespace, and a host tmpfs bindmounted
> into a container is trusted even though the container can write to it.
> This is VSTOR-141322.
> 
> Patch 3 fixes ownership being wrong at creation. A new mount takes its
> owner from get_exec_env(), the VE of the task, rather than the VE of the
> mount namespace being worked in. Those differ for a ve0 task that entered
> a container's mount namespace with nsenter -m. copy_mnt_ns() and
> open_detached_copy() both build mounts that way and neither reaches
> commit_tree(), so a plain unshare(CLONE_NEWNS) or an
> open_tree(OPEN_TREE_CLONE) hands back ve0 owned copies of container
> content. This is VSTOR-141429.
> 
> Patches 2 and 3 apply the same rule, the one already used when a namespace
> is copied at container creation: ownership comes from the mount namespace,
> never from the calling task.
> 
Generally LGTM. The only problematic scenario I see now that we gate
host execution based on namespace is that we could have tmpfs with two
mounts one in host and one in CT. CT can still tamper with things and on
the fs and ve_check_trusted_file will still return true allowing us to
execute on host. I don't thing this should be covered in this series as its
irrelevant I just wanted to mention it in case other reviewers think we
need to do something about it. If deemed necessary we could solve it
in O(1) time, by using some superblock flag and checking against that
in ve_check_trusted_file.
> Testing
> =======
> 
> Built on 6.12.0-211.39.1.16.4.vz10, debug flavour, with KASAN, lockdep,
> PROVE_LOCKING, DEBUG_ATOMIC_SLEEP, DEBUG_LIST, DEBUG_VM and gcov on
> fs/namespace.o. Also compiled with CONFIG_VE=n. Each commit builds
> standalone, so bisect is safe.
> 
> Before and after on the same host, same commands. Stock
> 6.12.0-211.30.1.14.4.vz10 on the left, the series on the right:
> 
>   container at its mount limit, unshare(CLONE_NEWNS)  PANIC    -> survives
>   CT tmpfs, plain nsenter (control)                   refused  -> refused
>   nsenter + unshare -m                                EXECUTED -> refused
>   nsenter + open_tree + execveat                      EXECUTED -> refused
>   host tmpfs lent via --bindmount_add                 EXECUTED -> refused
> 
> The panic was captured on stock over netconsole: "list_add corruption ...
> kernel BUG at lib/list_debug.c:32", Comm: unshare, ve: 900, trace
> alloc_mnt_ns <- copy_mnt_ns <- unshare. Against the series the identical
> sequence, 150 container unshares at the limit followed by 300 host
> namespace creations, completes with no corruption and no dump. The
> control case behaving the same on both kernels shows the difference is
> the change and not the environment.
> 
> No KASAN, lockdep, atomic sleep, refcount or list corruption reports
> across any of it. DEBUG_ATOMIC_SLEEP staying quiet covers the one thing
> worth asking about in patch 2, that get_ve()/put_ve() are called under
> lock_mount_hash().
> 
> ve.mnt_nr drifted by 0 over 15 bindmount add and remove cycles, and
> nr_dying_descendants moved by 1 across 150 opportunities to leak a VE
> reference, so patch 1 releases the reference rather than merely not
> crashing.
> 
> Coverage from gcov, so the new code is known to have run rather than just
> linked: ve_mount_reown() called 54300 times with the ownership transfer
> branch taken 958 times, and both commit_tree() call sites exercised,
> 20024 for the moved tree and 24081 for the propagation loop.
> 
> selftests: mount 2/2, mount_setattr 21/21, ve_perms 14/14, ve_ns_owner
> 2/2. The last one matters most, it asserts ve.mnt_nr behaviour around
> CLONE_NEWVE, which is the line patch 3 changes.
> 
> vzctl functional suite: 538 of 603 passed. About 25 of the failures are
> vzctl returning exit 21 where the suite asserts 20 for an unrecognized
> option, which never reaches the kernel. The other ten were re-run one at
> a time: two passed, and the rest fail in container creation or disk setup
> on a test filesystem out of space, not on the mount operations they
> exercise.
> 
> KCSAN, on a separate build of the same series with KCSAN enabled at
> runtime: 2520 reports over a 15 minute run, drained continuously so that
> is every report rather than what happened to survive in the ring buffer,
> while KCSAN's own counter went from 5174 to 10179 data races. None of
> them names ve_owner, commit_tree(), ve_check_trusted_file(),
> is_sb_ve_accessible() or mnt_ns_unlink(). The code carrying the new store
> was hot and instrumented throughout: attach_recursive_mnt(), which calls
> commit_tree(), appears in 201 stack traces, and 193 of the reports are
> races in propagate_one() and propagate_mnt() beside it. Those are
> pre-existing upstream races on inode and mount fields, not on ve_owner.
> 
> Not addressed here
> ==================
> 
> Patch 2 lets a container be pushed above sysctl_ve_mount_nr by mounts the
> host gives it, and while over it the container's own mounts are refused
> until the count drops. The default limit is 4096 so this takes an unusual
> number of lent mounts, and the container can unmount them, but it is the
> host's action that spends the container's budget. Enforcing the limit at
> handover is possible, attach_recursive_mnt() already does the equivalent
> for sysctl_mount_max in count_mounts(), but ve_mount_allowed() tests the
> creating task's VE while the counter follows the owner, so the limit
> currently has two meanings and picking one is a separate decision.
> 
> Mirian Shilakadze (3):
>   ve/fs: unlink the mount namespace on the copy_mnt_ns() error path
>   ve/fs: transfer mount ownership when a mount enters another VE
>   ve/fs: take the owner of copied mounts from the namespace, not the task
> 
>  fs/mount.h     |  2 +-
>  fs/namespace.c | 73 ++++++++++++++++++++++++++++++++++++++++++++------
>  kernel/ve/ve.c |  6 ++++-
>  3 files changed, 71 insertions(+), 10 deletions(-)
> 
> --
> 2.43.0
> _______________________________________________
> Devel mailing list
> Devel at openvz.org
> https://lists.openvz.org/mailman/listinfo/devel

Reviewed-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>

-- 
Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>

^ permalink raw reply	[relevance 0%]

* [Devel] [PATCH VZ10 5/5] drivers/vhost/blk: rework queue/backend setup
  @ 2026-08-18 15:09  7% ` Andrey Zhadchenko
  0 siblings, 0 replies; 200+ results
From: Andrey Zhadchenko @ 2026-08-18 15:09 UTC (permalink / raw)


vhost_blk_setup() is pretty bad: silently refusing changed vq->num
if requests are already allocated, fetching user input second time
(double-fetch vulnerability).
To handle this, tie request allocation to backend existence. After
all, if there is no backend, there is no point in having requests.
Also expand it to get rid of boilerplate drop_backend, flush,
fput sequence in a few places.

https://virtuozzo.atlassian.net/browse/VSTOR-138640
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
 drivers/vhost/blk.c | 126 +++++++++++++++++++-------------------------
 1 file changed, 55 insertions(+), 71 deletions(-)

diff --git a/drivers/vhost/blk.c b/drivers/vhost/blk.c
index 2f94c987c62d8..fd12d4ee317a0 100644
--- a/drivers/vhost/blk.c
+++ b/drivers/vhost/blk.c
@@ -653,11 +653,14 @@ static void vhost_blk_flush(struct vhost_blk *blk)
 	spin_unlock(&blk->flush_lock);
 }
 
-static inline void vhost_blk_drop_backends(struct vhost_blk *blk)
+static void vhost_blk_drop_backend(struct vhost_blk *blk)
 {
 	struct vhost_virtqueue *vq;
 	int i;
 
+	if (!blk->backend)
+		return;
+
 	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
 		vq = &blk->vqs[i].vq;
 
@@ -665,6 +668,45 @@ static inline void vhost_blk_drop_backends(struct vhost_blk *blk)
 		vhost_vq_set_backend(vq, NULL);
 		mutex_unlock(&vq->mutex);
 	}
+
+	vhost_blk_flush(blk);
+
+	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
+		kvfree(blk->vqs[i].req);
+		blk->vqs[i].req = NULL;
+	}
+
+	fput(blk->backend);
+	blk->backend = NULL;
+}
+
+static int vhost_blk_setup_vqs(struct vhost_blk *blk)
+{
+	struct vhost_virtqueue *vq;
+	int i;
+
+	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
+		vq = &blk->vqs[i].vq;
+
+		if (!vhost_vq_is_setup(vq))
+			continue;
+
+		blk->vqs[i].req = kvmalloc_array(vq->num, sizeof(struct vhost_blk_req),
+						 GFP_KERNEL);
+		if (!blk->vqs[i].req)
+			return -ENOMEM;
+
+		mutex_lock(&vq->mutex);
+		vhost_vq_set_backend(vq, blk->backend);
+		if (vhost_vq_init_access(vq)) {
+			mutex_unlock(&vq->mutex);
+			return -EFAULT;
+		}
+		mutex_unlock(&vq->mutex);
+
+	}
+
+	return 0;
 }
 
 static int vhost_blk_open(struct inode *inode, struct file *file)
@@ -717,16 +759,10 @@ static int vhost_blk_open(struct inode *inode, struct file *file)
 static int vhost_blk_release(struct inode *inode, struct file *f)
 {
 	struct vhost_blk *blk = f->private_data;
-	int i;
 
-	vhost_blk_drop_backends(blk);
-	vhost_blk_flush(blk);
+	vhost_blk_drop_backend(blk);
 	vhost_dev_stop(&blk->dev);
-	if (blk->backend)
-		fput(blk->backend);
 	vhost_dev_cleanup(&blk->dev);
-	for (i = 0; i < VHOST_BLK_VQ_MAX; i++)
-		kvfree(blk->vqs[i].req);
 	kfree(blk->dev.vqs);
 	kvfree(blk);
 
@@ -760,32 +796,19 @@ static int vhost_blk_set_features(struct vhost_blk *blk, u64 features)
 
 static long vhost_blk_set_backend(struct vhost_blk *blk, int fd)
 {
-	struct vhost_virtqueue *vq;
 	struct file *file;
 	struct inode *inode;
-	int ret, i;
+	int ret;
 
 	mutex_lock(&blk->dev.mutex);
 	ret = vhost_dev_check_owner(&blk->dev);
 	if (ret)
 		goto out_dev;
 
-	/*
-	 * fd < 0 means "stop the device".  Detach the backend from every vq so
-	 * vhost_blk_handle_guest_kick() stops fetching descriptors, drain the
-	 * in-flight requests, and release the backing file.
-	 */
+	/* fd < 0 means "stop the device" */
 	if (fd < 0) {
-		if (!blk->backend) {
-			ret = 0;		/* already stopped */
-			goto out_dev;
-		}
-		vhost_blk_drop_backends(blk);
-		vhost_blk_flush(blk);
-		fput(blk->backend);
-		blk->backend = NULL;
 		ret = 0;
-		goto out_dev;
+		goto out_drop;
 	}
 
 	if (blk->backend) {
@@ -802,31 +825,21 @@ static long vhost_blk_set_backend(struct vhost_blk *blk, int fd)
 	inode = file->f_mapping->host;
 	if (!S_ISBLK(inode->i_mode)) {
 		ret = -EFAULT;
-		goto out_file;
+		fput(file);
+		goto out_dev;
 	}
 
-	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
-		vq = &blk->vqs[i].vq;
-		if (!vhost_vq_access_ok(vq)) {
-			ret = -EFAULT;
-			goto out_drop;
-		}
-
-		mutex_lock(&vq->mutex);
-		vhost_vq_set_backend(vq, file);
-		ret = vhost_vq_init_access(vq);
-		mutex_unlock(&vq->mutex);
-	}
 
 	blk->backend = file;
+	ret = vhost_blk_setup_vqs(blk);
+	if (ret)
+		goto out_drop;
 
 	mutex_unlock(&blk->dev.mutex);
 	return 0;
 
 out_drop:
-	vhost_blk_drop_backends(blk);
-out_file:
-	fput(file);
+	vhost_blk_drop_backend(blk);
 out_dev:
 	mutex_unlock(&blk->dev.mutex);
 	return ret;
@@ -835,7 +848,7 @@ static long vhost_blk_set_backend(struct vhost_blk *blk, int fd)
 static long vhost_blk_reset_owner(struct vhost_blk *blk)
 {
 	struct vhost_iotlb *umem;
-	int err, i;
+	int err;
 
 	mutex_lock(&blk->dev.mutex);
 	err = vhost_dev_check_owner(&blk->dev);
@@ -846,42 +859,15 @@ static long vhost_blk_reset_owner(struct vhost_blk *blk)
 		err = -ENOMEM;
 		goto done;
 	}
-	vhost_blk_drop_backends(blk);
-	if (blk->backend) {
-		fput(blk->backend);
-		blk->backend = NULL;
-	}
-	vhost_blk_flush(blk);
+	vhost_blk_drop_backend(blk);
 	vhost_dev_stop(&blk->dev);
 	vhost_dev_reset_owner(&blk->dev, umem);
 
-	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
-		kvfree(blk->vqs[i].req);
-		blk->vqs[i].req = NULL;
-	}
-
 done:
 	mutex_unlock(&blk->dev.mutex);
 	return err;
 }
 
-static int vhost_blk_setup(struct vhost_blk *blk, void __user *argp)
-{
-	struct vhost_vring_state s;
-
-	if (copy_from_user(&s, argp, sizeof(s)))
-		return -EFAULT;
-
-	if (blk->vqs[s.index].req)
-		return 0;
-
-	blk->vqs[s.index].req = kvmalloc(sizeof(struct vhost_blk_req) * s.num, GFP_KERNEL);
-	if (!blk->vqs[s.index].req)
-		return -ENOMEM;
-
-	return 0;
-}
-
 static long vhost_blk_ioctl(struct file *f, unsigned int ioctl,
 			    unsigned long arg)
 {
@@ -919,8 +905,6 @@ static long vhost_blk_ioctl(struct file *f, unsigned int ioctl,
 		ret = vhost_dev_ioctl(&blk->dev, ioctl, argp);
 		if (ret == -ENOIOCTLCMD)
 			ret = vhost_vring_ioctl(&blk->dev, ioctl, argp);
-		if (!ret && ioctl == VHOST_SET_VRING_NUM)
-			ret = vhost_blk_setup(blk, argp);
 		vhost_blk_flush(blk);
 		mutex_unlock(&blk->dev.mutex);
 		return ret;
-- 
2.43.5


^ permalink raw reply	[relevance 7%]

* Re: [Devel] [PATCH vz10 3/5] fixup! vhost/vsock: only refuse connection when guest has never been ready
       [not found]     ` <20260625181637.1555685-3-eva.kurchatova@virtuozzo.com>
@ 2026-08-18 16:53  4%   ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-18 16:53 UTC (permalink / raw)


On 6/25/26 20:16, Eva Kurchatova wrote:
> From: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
> 
> Commit 4ff28534c799 ("ms/vhost/vsock: Refuse the connection immediately
> when guest isn't ready") added a check which immediately returns
> EHOSTUNREACH when the guest isn't ready yet.  Namely, we check that guest
> hasn't enabled the RX vq yet, i.e. virtio-vsock has never beed enabled.

s/beed/been/

> 
> However, the check also affects the transient state when backend is
> temporarily set to NULL during VHOST_VSOCK_SET_RUNNING(0).  Notably,
> this is the case with qemu-update operation, during which we perform
> VHOST_RESET_OWNER.  In this case sendmsg()/connect() on otherwise healthy
> connection gets EHOSTUNREACH.
> 
> Gate the fast-fail on a sticky started_once bit set in
> vhost_vsock_start() and never cleared.  Once the guest has brought
> up virtio-vsock at least once, a NULL backend means a transient stop
> window and the packet must be queued for vhost_vsock_start() to drain
> on re-attach.
> 
> Fixes: 4ff28534c799 ("ms/vhost/vsock: Refuse the connection immediately when guest isn't ready")
> https://virtuozzo.atlassian.net/browse/VSTOR-131956
> Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
> ---
>  drivers/vhost/vsock.c | 31 ++++++++++++++-----------------
>  1 file changed, 14 insertions(+), 17 deletions(-)
> 
> diff --git a/drivers/vhost/vsock.c b/drivers/vhost/vsock.c
> index d4c3f94308db..f652f47956d7 100644
> --- a/drivers/vhost/vsock.c
> +++ b/drivers/vhost/vsock.c
> @@ -59,6 +59,7 @@ struct vhost_vsock {
>  
>  	u32 guest_cid;
>  	bool seqpacket_allow;
> +	bool started_once;	/* latched in vhost_vsock_start(); never cleared */
>  	bool cpr_paused;	/* between stop and next start; queues sends */
>  };

static int vhost_vsock_dev_open(struct inode *inode, struct file *file)
{
...
        vsock = kvmalloc(sizeof(*vsock), GFP_KERNEL | __GFP_RETRY_MAYFAIL);

So, not zeroing struct fields here on allocation.
.started_once is not directly initialized => can have any start value.

>  
> @@ -289,24 +290,17 @@ vhost_transport_send_pkt(struct sk_buff *skb, struct net *net)
>  		return -ENODEV;
>  	}
>  
> -	/* Fast-fail if the guest hasn't enabled the RX vq yet. Queuing the packet
> -	 * and making the caller wait is pointless: even if the guest manages to init
> -	 * within the timeout, it'll immediately reply with RST, because there's no
> -	 * listener on the port yet.
> -	 *


> -	 * vhost_vq_get_backend() without vq->mutex is acceptable here: locking
> -	 * the mutex would be too expensive in this hot path, and we already have
> -	 * all the outcomes covered: if the backend becomes NULL right after the check,
> -	 * vhost_transport_do_send_pkt() will check it under the mutex anyway.

Why you drop this part of the comment? It looks useful.

> +	/*
> +	 * Fast-fail only when the guest has never enabled virtio-vsock.
> +	 * Once it has, a NULL backend means a transient SET_RUNNING(0)
> +	 * window (e.g. VHOST_RESET_OWNER); the packet must be
> +	 * queued for vhost_vsock_start() to drain on re-attach.
>  	 */
> -	/* cpr_paused: queue across CPR; else NULL backend means not ready. */
> -	if (unlikely(!data_race(vhost_vq_get_backend(&vsock->vqs[VSOCK_VQ_RX])))) {
> -		smp_rmb();	/* pairs with smp_wmb() in start/drop_backends */
> -		if (!READ_ONCE(vsock->cpr_paused)) {

(kostja at f0)/git/vzkernel.vz10:git grep cpr_paused
drivers/vhost/vsock.c:  bool cpr_paused;        /* between stop and next start; queues sends */
drivers/vhost/vsock.c:  /* cpr_paused: queue across CPR; else NULL backend means not ready. */
drivers/vhost/vsock.c:          if (!READ_ONCE(vsock->cpr_paused)) {
drivers/vhost/vsock.c:  WRITE_ONCE(vsock->cpr_paused, false);
drivers/vhost/vsock.c:          WRITE_ONCE(vsock->cpr_paused, true);
drivers/vhost/vsock.c:  vsock->cpr_paused = false;

So you are dropping the only READ of this vsock->cpr_paused, so it's not needed anymore after this patch.

> -			rcu_read_unlock();
> -			kfree_skb(skb);
> -			return -EHOSTUNREACH;
> -		}
> +	if (unlikely(!READ_ONCE(vsock->started_once)) &&
> +	    !data_race(vhost_vq_get_backend(&vsock->vqs[VSOCK_VQ_RX]))) {
> +		rcu_read_unlock();
> +		kfree_skb(skb);
> +		return -EHOSTUNREACH;
>  	}
>  
>  	if (virtio_vsock_skb_reply(skb))
> @@ -637,6 +631,9 @@ static int vhost_vsock_start(struct vhost_vsock *vsock)
>  	 */
>  	vhost_vq_work_queue(&vsock->vqs[VSOCK_VQ_RX], &vsock->send_pkt_work);
>  
> +	/* See vhost_transport_send_pkt(); never cleared. */
> +	WRITE_ONCE(vsock->started_once, true);
> +
>  	mutex_unlock(&vsock->dev.mutex);
>  	return 0;
>  


^ permalink raw reply	[relevance 4%]

* [Devel] [PATCH VZ10 v6 3/9] ve/fs: Rework per-ve mount count
  @ 2026-08-19  9:07  3% ` Vladimir Riabchun
  2026-08-19 13:16  0%   ` Vasileios Almpanis
  2026-08-19  9:07  8% ` [Devel] [PATCH VZ10 v6 7/9] ve: Introduce per-VE failcount Vladimir Riabchun
                   ` (2 subsequent siblings)
  3 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-08-19  9:07 UTC (permalink / raw)


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     | 71 +++++++++++++++++++++++++++-------------------
 include/linux/ve.h |  2 +-
 kernel/ve/ve.c     | 12 ++++----
 3 files changed, 49 insertions(+), 36 deletions(-)

diff --git a/fs/namespace.c b/fs/namespace.c
index 68e0efb73d7c..c9e2ab9b3b57 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -317,18 +317,21 @@ 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 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;
 	}
 
@@ -336,6 +339,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;
@@ -370,7 +381,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;
 
@@ -381,6 +393,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;
 }
@@ -750,7 +764,7 @@ 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);
+	ve_mount_put(mnt, mnt->ve_owner);
 	kfree_const(mnt->mnt_devname);
 #ifdef CONFIG_SMP
 	free_percpu(mnt->mnt_pcp);
@@ -3205,7 +3219,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.
 			 */
@@ -3344,30 +3358,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)
+static inline int ve_try_reserve_mount(struct ve_struct *ve)
 {
-	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)
-{
-	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;
 }
 
 bool is_sb_ve_accessible(struct ve_struct *ve, struct super_block *sb)
@@ -3389,9 +3403,8 @@ 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) { }
 #endif /* CONFIG_VE */
 
 /*
diff --git a/include/linux/ve.h b/include/linux/ve.h
index 3facbd1759df..cca0a2bc1aac 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -88,7 +88,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 dffb35da22bd..42669a832993 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -81,7 +81,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),
@@ -778,7 +778,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");
@@ -1054,9 +1054,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)
@@ -1616,8 +1616,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


^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH VZ10 v6 7/9] ve: Introduce per-VE failcount
    2026-08-19  9:07  3% ` [Devel] [PATCH VZ10 v6 3/9] ve/fs: Rework per-ve mount count Vladimir Riabchun
@ 2026-08-19  9:07  8% ` Vladimir Riabchun
  2026-08-19 13:16  0%   ` Vasileios Almpanis
  2026-08-19  9:07  6% ` [Devel] [PATCH VZ10 v6 8/9] selftests/ve: Add more helpers Vladimir Riabchun
  2026-08-19  9:07  5% ` [Devel] [PATCH VZ10 v6 9/9] selftests/ve: Add mount accounting selftest Vladimir Riabchun
  3 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-08-19  9:07 UTC (permalink / raw)


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       |  6 ++++
 kernel/bpf/syscall.c     |  1 +
 kernel/ve/ve.c           | 67 ++++++++++++++++++++++++++++++++++++++++
 net/core/dev.c           |  2 ++
 net/core/neighbour.c     |  1 +
 net/core/net_namespace.c |  4 ++-
 8 files changed, 83 insertions(+), 1 deletion(-)

diff --git a/fs/aio.c b/fs/aio.c
index cb63416af135..3fa07cc626f8 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) {
+		atomic_inc(&ve->aio_failcount);
 		spin_unlock(&ve->aio_nr_lock);
 		err = -EAGAIN;
 		goto err_ctx;
diff --git a/fs/namespace.c b/fs/namespace.c
index c9e2ab9b3b57..eeeb2f780e46 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -3369,6 +3369,8 @@ static inline int ve_try_reserve_mount(struct ve_struct *ve)
 
 	if (ret)
 		get_ve(ve);
+	else
+		atomic_inc(&ve->mnt_failcount);
 	return ret;
 }
 
diff --git a/include/linux/ve.h b/include/linux/ve.h
index 5687faad46ff..9e73527e970e 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -72,12 +72,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;
@@ -86,6 +89,7 @@ struct ve_struct {
 
 	atomic_t		arp_neigh_nr;
 	atomic_t		nd_neigh_nr;
+	atomic_t		neigh_tbl_failcount;
 	unsigned long		meminfo_val;
 
 	/*
@@ -94,6 +98,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
@@ -121,6 +126,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;
 };
diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
index c94d4240e3d3..9d57e7999ae0 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) {
+			atomic_inc(&load_ve->bpf_prog_failcount);
 			load_ve = NULL;
 			err = -ENOSPC;
 			goto put_token;
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index 0f02835765ff..55a83b0b5981 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -99,10 +99,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	=
@@ -114,8 +117,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,
 	.umh_running_helpers	= ATOMIC_INIT(0),
 	.umh_helpers_waitq	= __WAIT_QUEUE_HEAD_INITIALIZER(ve0.umh_helpers_waitq),
@@ -780,12 +791,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);
@@ -812,7 +826,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
@@ -825,6 +841,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;
@@ -1065,6 +1082,50 @@ 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;
+
+	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);
@@ -1602,6 +1663,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..05e0b9b6ba23 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -10997,6 +10997,7 @@ int register_netdevice(struct net_device *dev)
 
 	ret = -ENOMEM;
 	if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
+		atomic_inc(&net->owner_ve->netif_failcount);
 		ve_pr_warn_ratelimited(VE_LOG_BOTH,
 			"CT%s: hits max number of network devices, "
 			"increase ve::netif_max_nr parameter\n",
@@ -12211,6 +12212,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) {
+		atomic_inc(&net->owner_ve->netif_failcount);
 		ve_pr_warn_ratelimited(VE_LOG_BOTH,
 			"CT%s: hits max number of network devices, "
 			"increase ve::netif_max_nr parameter\n",
diff --git a/net/core/neighbour.c b/net/core/neighbour.c
index f90deb17fb25..57a49d9c98a7 100644
--- a/net/core/neighbour.c
+++ b/net/core/neighbour.c
@@ -520,6 +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) {
+			atomic_inc(&ve->neigh_tbl_failcount);
 			net_info_ratelimited("%s: neighbor table overflow!\n",
 					     tbl->id);
 			NEIGH_CACHE_STAT_INC(tbl, table_fulls);
diff --git a/net/core/net_namespace.c b/net/core/net_namespace.c
index b3d54cad984a..9a3376d2682f 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) {
+		atomic_inc(&ve->netns_failcount);
 		return -ENOSPC;
+	}
 	return 0;
 }
 
-- 
2.47.1


^ permalink raw reply	[relevance 8%]

* [Devel] [PATCH VZ10 v6 8/9] selftests/ve: Add more helpers
    2026-08-19  9:07  3% ` [Devel] [PATCH VZ10 v6 3/9] ve/fs: Rework per-ve mount count Vladimir Riabchun
  2026-08-19  9:07  8% ` [Devel] [PATCH VZ10 v6 7/9] ve: Introduce per-VE failcount Vladimir Riabchun
@ 2026-08-19  9:07  6% ` Vladimir Riabchun
  2026-08-19  9:07  5% ` [Devel] [PATCH VZ10 v6 9/9] selftests/ve: Add mount accounting selftest Vladimir Riabchun
  3 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-08-19  9:07 UTC (permalink / raw)


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-135520

Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
 tools/testing/selftests/ve/ve_selftest.h | 81 ++++++++++++++++++++++--
 1 file changed, 75 insertions(+), 6 deletions(-)

diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index 69c0a52dd7ef..48bb7d1871bd 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -43,6 +43,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[20];
+
+	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)
 {
@@ -73,19 +81,31 @@ static inline int read_u64_at(int dirfd, const char *path,
 			      unsigned long long *out)
 {
 	char buf[32] = {0}, *end;
-	int fd, ret;
+	int ret;
 
-	fd = openat(dirfd, path, O_RDONLY);
-	if (fd < 0)
+	ret = read_file_at(dirfd, path, buf, sizeof(buf));
+	if (ret <= 0)
 		return -1;
 
-	ret = read(fd, buf, sizeof(buf) - 1);
-	close(fd);
+	errno = 0;
+	*out = strtoull(buf, &end, 10);
+	if (errno || end == buf)
+		return -1;
+	return 0;
+}
+
+static inline int read_s32_at(int dirfd, const char *path,
+			      int *out)
+{
+	char buf[32] = {0}, *end;
+	int ret;
+
+	ret = read_file_at(dirfd, path, buf, sizeof(buf));
 	if (ret <= 0)
 		return -1;
 
 	errno = 0;
-	*out = strtoull(buf, &end, 10);
+	*out = strtol(buf, &end, 10);
 	if (errno || end == buf)
 		return -1;
 	return 0;
@@ -134,6 +154,55 @@ static inline int enter_cgroup(int cgv2_fd, int ctid)
 	return ret;
 }
 
+/*
+ * Run function in VE cgroup and new namespaces.
+ *
+ * Namespaces are provided via unshare_flags.
+ * CLONE_NEWVE flag is set by this function.
+ * Return values:
+ *  -  0 if function returns zero
+ *  - -1 if function returns negative value
+ *  -  1 if setup fails or function returns positive value
+ */
+static inline int run_in_ve(int cgv2_fd, int ctid, int unshare_flags,
+		int (*fn)(void *), void *arg)
+{
+	int status;
+	pid_t pid;
+
+	unshare_flags |= CLONE_NEWVE;
+	pid = fork();
+	if (pid < 0) {
+		fprintf(stderr, "%s: fork failed\n", __func__);
+		return 1;
+	}
+	if (pid == 0) {
+		int ret;
+
+		if (enter_cgroup(cgv2_fd, ctid) < 0) {
+			fprintf(stderr, "%s: enter_cgroup failed\n", __func__);
+			_exit(255);
+		}
+		if (unshare(unshare_flags) < 0) {
+			fprintf(stderr, "%s: unshare(%d) failed\n",
+				__func__, unshare_flags);
+			_exit(255);
+		}
+		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) == 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.47.1


^ permalink raw reply	[relevance 6%]

* [Devel] [PATCH VZ10 v6 9/9] selftests/ve: Add mount accounting selftest
                     ` (2 preceding siblings ...)
  2026-08-19  9:07  6% ` [Devel] [PATCH VZ10 v6 8/9] selftests/ve: Add more helpers Vladimir Riabchun
@ 2026-08-19  9:07  5% ` Vladimir Riabchun
  2026-08-19 13:16  0%   ` Vasileios Almpanis
  3 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-08-19  9:07 UTC (permalink / raw)


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   | 421 ++++++++++++++++++
 3 files changed, 423 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..a270a8cb8c51
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_mount_accounting_test.c
@@ -0,0 +1,421 @@
+// 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
+
+static int set_pseudosuper(int cgv2_fd, int ctid, int value)
+{
+	char path[64];
+
+	snprintf(path, sizeof(path), "%d/ve.pseudosuper", ctid);
+	return write_u64_at(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 ret;
+}
+
+static int create_mount(int cgv2_fd, int ctid, int id)
+{
+	int ret = run_in_ve(cgv2_fd, ctid, CLONE_NEWVE, _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(int cgv2_fd, int ctid, int id)
+{
+	int ret = run_in_ve(cgv2_fd, ctid, CLONE_NEWVE, _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(int cgv2_fd, int ctid)
+{
+	int avail1, avail2, mnt_id;
+	char path[64];
+
+	mnt_id = get_free_mnt_id();
+
+	snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", ctid);
+	if (mnt_id < 0 ||
+	    read_s32_at(cgv2_fd, path, &avail1) ||
+	    create_mount(cgv2_fd, ctid, mnt_id) ||
+	    read_s32_at(cgv2_fd, path, &avail2) ||
+	    destroy_mount(cgv2_fd, ctid, mnt_id))
+		return -1;
+
+	return avail1 - avail2;
+}
+
+/* Expect mount success and return new avail value */
+static int mount_and_get_avail(struct __test_metadata *_metadata,
+			int cgv2_fd, int ctid, int mnt_id)
+{
+	char path_avail[64];
+	int mnt_avail_nr;
+
+	snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", ctid);
+
+	ASSERT_EQ(create_mount(cgv2_fd, ctid, mnt_id), 0);
+	ASSERT_EQ(read_s32_at(cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	return mnt_avail_nr;
+}
+
+/* Expect mount failure and ensure intact avail number */
+static void assert_mount_fails(struct __test_metadata *_metadata,
+			int cgv2_fd, int ctid, 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", ctid);
+
+	ASSERT_EQ(read_s32_at(cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, avail_count);
+	ASSERT_LT(create_mount(cgv2_fd, ctid, mnt_id), 0);
+	ASSERT_EQ(read_s32_at(cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, avail_count);
+}
+
+FIXTURE(ve_mnt_acc)
+{
+	int cgv2_fd;
+	int ctid;
+};
+
+FIXTURE_SETUP(ve_mnt_acc)
+{
+	unsigned long long 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_u64_at(self->cgv2_fd, path, &initial_mnt_avail_nr), 0);
+	ASSERT_EQ(initial_mnt_avail_nr, VE_MOUNTS_MAX);
+};
+
+FIXTURE_TEARDOWN(ve_mnt_acc)
+{
+	for (int i = 0; i < MAX_MNT_ID; i++)
+		_destroy_mount((void *)&i);
+
+	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->cgv2_fd, self->ctid, 0),
+		  original_mnt_avail);
+
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 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 original_mnt_avail, mnt_avail_nr, mnt_cost;
+	int original_have_mnt;
+	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);
+
+	mnt_cost = get_mount_cost(self->cgv2_fd, self->ctid);
+	ASSERT_GE(mnt_cost, 1);
+
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &original_mnt_avail), 0);
+	original_have_mnt = VE_MOUNTS_MAX - original_mnt_avail;
+
+	/* Step 1: reduce number of available mounts to mnt_cost */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, original_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 2: do one mount, no mounts should be available */
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 0),
+		  0);
+
+	/* Step 3: check that one more mount falils */
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 1, 0);
+
+	/* Step 4: increase mount limit a little bit, mount should still fail */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+				original_have_mnt + 2 * mnt_cost - 1), 0);
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 1, mnt_cost - 1);
+
+	/* Step 5: increase by 1 and win now */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, original_have_mnt + 2 * mnt_cost), 0);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, mnt_cost);
+
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 1),
+		  0);
+
+	/* Step 6: reduce mnt_max_nr so we have more mounts than allowed */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, original_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 7: try to do mount when avail < 0, ensure number is intact */
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, -1 * mnt_cost);
+
+	/* Step 8: remove one mount, check avail value update, mount should fail */
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 0), 0);
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, 0);
+
+	/* Step 9: remove one more mount and check that new mount succeeds */
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 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(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 2),
+		  0);
+
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 2), 0);
+}
+
+/*
+ * Mount propagation makes one mount cost more.
+ * This test check that if we run out or 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 mount_cost, i, orig_have, orig_mnt_avail;
+
+	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);
+
+	mount_cost = get_mount_cost(self->cgv2_fd, self->ctid);
+	ASSERT_GE(mount_cost, 1);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &orig_mnt_avail), 0);
+	orig_have = VE_MOUNTS_MAX - orig_mnt_avail;
+
+	if (mount_cost == 1)
+		SKIP(return, "mount cost is 1, no partial mounts possible");
+
+	for (i = 0; i < mount_cost; i++) {
+		ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, orig_have + i), 0);
+		assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 0, i);
+	}
+}
+
+/* Test that pseudosuper allows negative avail with correct accounting. */
+TEST_F(ve_mnt_acc, pseudosuper_allows_overuse)
+{
+	int orig_mnt_avail, orig_have;
+	int mount_cost;
+	char path_avail[64], path_max_nr[64];
+
+	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);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &orig_mnt_avail), 0);
+	orig_have = VE_MOUNTS_MAX - orig_mnt_avail;
+
+	mount_cost = get_mount_cost(self->cgv2_fd, self->ctid);
+	ASSERT_GE(mount_cost, 1);
+
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, orig_have + mount_cost), 0);
+	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 1), 0);
+
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 0),
+		  0);
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 1),
+		  -1 * mount_cost);
+
+	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 0), 0);
+
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, -1 * mount_cost);
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 0), 0);
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, 0);
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 1), 0);
+	ASSERT_EQ(get_mount_cost(self->cgv2_fd, self->ctid), mount_cost);
+}
+
+/* Test that pseudosuper doesn't disable accounting. */
+TEST_F(ve_mnt_acc, pseudosuper_continues_accounting)
+{
+	int orig_mnt_avail, mount_cost, 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);
+	mount_cost = get_mount_cost(self->cgv2_fd, self->ctid);
+	ASSERT_GE(mount_cost, 1);
+
+	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 0), 0);
+
+	/* mnt 0 - mounted without pseudosuper, umounted with it. */
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 0),
+		  orig_mnt_avail - mount_cost);
+	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 1), 0);
+
+	/* Cost is the same when mount/umount happen under pseudosuper. */
+	ASSERT_EQ(get_mount_cost(self->cgv2_fd, self->ctid), mount_cost);
+
+	/* mnt 1 - mounted with pseudosuper, umounted without it. */
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 1),
+		  orig_mnt_avail - 2 * mount_cost);
+
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 0), 0);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, orig_mnt_avail - mount_cost);
+
+	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 0), 0);
+
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 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->cgv2_fd, self->ctid, 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->cgv2_fd, self->ctid, 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->cgv2_fd, self->ctid, 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->cgv2_fd, self->ctid, 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);
+}
+
+TEST_HARNESS_MAIN
-- 
2.47.1


^ permalink raw reply	[relevance 5%]

* Re: [Devel] [PATCH VZ10 v2 01/10] drivers/md/dm-qcow2: fix revert_cluster_alloc() for ext_l2 case
  @ 2026-08-19 10:27  4%   ` Pavel Tikhomirov
  0 siblings, 0 replies; 200+ results
From: Pavel Tikhomirov @ 2026-08-19 10:27 UTC (permalink / raw)




On 8/12/26 22:19, Andrey Zhadchenko wrote:
> This function walks over all changed u64 values in md. With ext_l2
> half of them holds subcluster description. Firstly it reverts these
> values to saved pe_page, which is fine, but then it tries to revert
> r1r2 changes. It makes no sense when the value is a subcluster
> description.
> Teach the function to skip subcluster descriptions: do it based on
> a new lx_level in struct wb_desc. Add new argument to
> prepare_l_entry_update() and set it there.
> The warning also could have tripped for ext_l2 entries, so drop
> it entirely.
> The function effectively reverts not only cluster alloc, so rename
> it to revert_l_entries_update() to mimic prepare_l_entries_update.

prepare_l_entrie(s)_update is probably a misspelled prepare_l_entry_update

> 
> Feature: dm-qcow2: block device over QCOW2 files driver
> https://virtuozzo.atlassian.net/browse/VSTOR-139406
> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
> ---
>  drivers/md/dm-qcow2-map.c    | 28 ++++++++++++++++++----------
>  drivers/md/dm-qcow2-target.c |  2 +-
>  drivers/md/dm-qcow2.h        |  1 +
>  3 files changed, 20 insertions(+), 11 deletions(-)
> 
> diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
> index db21efb45e17a..2cd8e174049f8 100644
> --- a/drivers/md/dm-qcow2-map.c
> +++ b/drivers/md/dm-qcow2-map.c
> @@ -1297,18 +1297,19 @@ static void do_md_page_read_complete(int ret, struct qcow2 *qcow2,
>  }
>  
>  /* Be careful with dirty_or_writeback()/etc! Check races. */
> -static void revert_clusters_alloc(struct qcow2 *qcow2, struct wb_desc *wbd)
> +static void revert_l_entries_update(struct qcow2 *qcow2, struct wb_desc *wbd)
>  {
>  	struct qcow2_map_item r1, r2;
>  	struct page *pe_page;
> +	bool skip_odd;
>  	u64 pos, old;
>  	int i, ret;
>  
> +	skip_odd = qcow2->ext_l2 && wbd->lx_level == L2_LEVEL;
> +
>  	lockdep_assert_held(&qcow2->md_pages_lock);
>  	for_each_set_bit(i, wbd->changed_indexes, LX_INDEXES_PER_PAGE) {
>  		pos = get_u64_from_be_page(wbd->md->page, i);
> -		WARN_ON_ONCE(!(pos & ~LX_REFCOUNT_EXACTLY_ONE) ||
> -			     !(pos & LX_REFCOUNT_EXACTLY_ONE));
>  
>  		/* Here we restore prealloced and compressed clu mappings */
>  		pe_page = wbd->pe_page;
> @@ -1321,6 +1322,9 @@ static void revert_clusters_alloc(struct qcow2 *qcow2, struct wb_desc *wbd)
>  		}
>  
>  		set_u64_to_be_page(wbd->md->page, i, 0);
> +		if (skip_odd && (i & 1))
> +			continue; /* pos contains ext_l2 part of L2 entry */
> +
>  		spin_unlock(&qcow2->md_pages_lock);
>  		pos &= ~LX_REFCOUNT_EXACTLY_ONE;
>  
> @@ -1369,7 +1373,7 @@ static void complete_wbd(struct qcow2 *qcow2, struct wb_desc *wbd)
>  		unsigned long flags;
>  
>  		spin_lock_irqsave(&qcow2->md_pages_lock, flags);
> -		revert_clusters_alloc(qcow2, wbd);
> +		revert_l_entries_update(qcow2, wbd);
>  		clear_writeback_status(qcow2, wbd->md, wbd->ret,
>  				       &wait_list, &end_list);
>  		spin_unlock_irqrestore(&qcow2->md_pages_lock, flags);
> @@ -2433,7 +2437,7 @@ static loff_t allocate_cluster(struct qcow2 *qcow2, struct qio *qio,
>  #define LU_IGN_CHANGED_IND	(1 << 3)
>  static int prepare_l_entry_update(struct qcow2 *qcow2, struct qio *qio,
>  				  struct md_page *md, u32 index_in_page,
> -				  u64 *pval, u32 arg_mask)
> +				  u64 *pval, u32 arg_mask, u8 lx_level)
>  {
>  	bool wants_pe_page = (arg_mask & LU_WANTS_PE_PAGE);
>  	struct wb_desc *new_wbd = NULL;
> @@ -2453,6 +2457,7 @@ static int prepare_l_entry_update(struct qcow2 *qcow2, struct qio *qio,
>  		if (!new_wbd)
>  			return -ENOMEM;
>  		new_wbd->md = md;
> +		new_wbd->lx_level = lx_level;
>  	} else if (wants_pe_page && !md->wbd->pe_page) {
>  		pe_page = alloc_page(GFP_NOIO|__GFP_ZERO);
>  		if (!pe_page)
> @@ -2515,7 +2520,8 @@ static int prepare_l1l2_allocation(struct qcow2 *qcow2, struct qio *qio,
>  		/* Allocate cluster for L2 entries, and prepare L1 update */
>  		ret = prepare_l_entry_update(qcow2, qio, map->l1.md,
>  					     map->l1.index_in_page, &val,
> -					     LU_SET_ONE_MASK|LU_WANTS_ALLOC);
> +					     LU_SET_ONE_MASK|LU_WANTS_ALLOC,
> +					     L1_LEVEL);
>  		if (ret <= 0)
>  			return ret;
>  
> @@ -2541,7 +2547,8 @@ static int prepare_l1l2_allocation(struct qcow2 *qcow2, struct qio *qio,
>  
>  		ret = prepare_l_entry_update(qcow2, qio, map->l2.md,
>  					     map->l2.index_in_page,
> -					     &map->data_clu_pos, arg_mask);
> +					     &map->data_clu_pos, arg_mask,
> +					     L2_LEVEL);
>  		if (ret <= 0)
>  			return ret;
>  
> @@ -2562,7 +2569,7 @@ static int prepare_l1l2_allocation(struct qcow2 *qcow2, struct qio *qio,
>  
>  	return prepare_l_entry_update(qcow2, qio, map->l2.md,
>  				      map->l2.index_in_page + 1,
> -				      &val, arg_mask);
> +				      &val, arg_mask, L2_LEVEL);
>  }
>  
>  /*
> @@ -3975,7 +3982,7 @@ static void process_cow_indexes_write(struct qcow2 *qcow2,
>  		ret = prepare_l_entry_update(qcow2, qio, lx_md,
>  					     ext->lx_index_in_page,
>  					     &ext->allocated_clu_pos,
> -					     arg_mask);
> +					     arg_mask, ext->cow_level);
>  		if (ret < 0) {
>  			qio->bi_status = errno_to_blk_status(ret);
>  			qio_endio(qio);
> @@ -3986,7 +3993,8 @@ static void process_cow_indexes_write(struct qcow2 *qcow2,
>  			arg_mask &= ~LU_SET_ONE_MASK;
>  			ret = prepare_l_entry_update(qcow2, qio, lx_md,
>  					     ext->lx_index_in_page + 1,
> -					   &ext->new_ext_l2, arg_mask);
> +					     &ext->new_ext_l2, arg_mask,
> +					     L2_LEVEL);
>  			WARN_ON_ONCE(ret < 0);
>  		}
>  
> diff --git a/drivers/md/dm-qcow2-target.c b/drivers/md/dm-qcow2-target.c
> index 3f65897ce9da2..0f1d3e3c5258e 100644
> --- a/drivers/md/dm-qcow2-target.c
> +++ b/drivers/md/dm-qcow2-target.c
> @@ -174,7 +174,7 @@ void qcow2_flush_deferred_activity(struct qcow2_target *tgt, struct qcow2 *qcow2
>  	int i;
>  
>  	/*
> -	 * We need second iteration, since revert_clusters_alloc()
> +	 * We need second iteration, since revert_l_entries_update()
>  	 * may start timer again after failed wb.
>  	 */
>  	for (i = 0; i < 2; i++) {
> diff --git a/drivers/md/dm-qcow2.h b/drivers/md/dm-qcow2.h
> index 86f0688e7345a..aa3487007523f 100644
> --- a/drivers/md/dm-qcow2.h
> +++ b/drivers/md/dm-qcow2.h
> @@ -118,6 +118,7 @@ struct wb_desc {
>  	struct list_head dependent_list;
>  	int nr_submitted;
>  	bool completed;
> +	u8 lx_level;
>  	int ret;
>  };
>  

-- 
Best regards, Pavel Tikhomirov
Senior Software Developer, Virtuozzo.


^ permalink raw reply	[relevance 4%]

* Re: [Devel] [PATCH VZ10 v6 7/9] ve: Introduce per-VE failcount
  2026-08-19  9:07  8% ` [Devel] [PATCH VZ10 v6 7/9] ve: Introduce per-VE failcount Vladimir Riabchun
@ 2026-08-19 13:16  0%   ` Vasileios Almpanis
  0 siblings, 0 replies; 200+ results
From: Vasileios Almpanis @ 2026-08-19 13:16 UTC (permalink / raw)


> 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..3fa07cc626f8 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) {
> +		atomic_inc(&ve->aio_failcount);
>  		spin_unlock(&ve->aio_nr_lock);
>  		err = -EAGAIN;
>  		goto err_ctx;
> diff --git a/fs/namespace.c b/fs/namespace.c
> index c9e2ab9b3b57..eeeb2f780e46 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
> @@ -3369,6 +3369,8 @@ static inline int ve_try_reserve_mount(struct ve_struct *ve)
>  
>  	if (ret)
>  		get_ve(ve);
> +	else
> +		atomic_inc(&ve->mnt_failcount);
>  	return ret;
>  }
>  
> diff --git a/include/linux/ve.h b/include/linux/ve.h
> index 5687faad46ff..9e73527e970e 100644
> --- a/include/linux/ve.h
> +++ b/include/linux/ve.h
> @@ -72,12 +72,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;
> @@ -86,6 +89,7 @@ struct ve_struct {
>  
>  	atomic_t		arp_neigh_nr;
>  	atomic_t		nd_neigh_nr;
> +	atomic_t		neigh_tbl_failcount;
>  	unsigned long		meminfo_val;
>  
>  	/*
> @@ -94,6 +98,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
> @@ -121,6 +126,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;
>  };
> diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
> index c94d4240e3d3..9d57e7999ae0 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) {
> +			atomic_inc(&load_ve->bpf_prog_failcount);
>  			load_ve = NULL;
>  			err = -ENOSPC;
>  			goto put_token;
> diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
> index 0f02835765ff..55a83b0b5981 100644
> --- a/kernel/ve/ve.c
> +++ b/kernel/ve/ve.c
> @@ -99,10 +99,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	=
> @@ -114,8 +117,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,
>  	.umh_running_helpers	= ATOMIC_INIT(0),
>  	.umh_helpers_waitq	= __WAIT_QUEUE_HEAD_INITIALIZER(ve0.umh_helpers_waitq),
> @@ -780,12 +791,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);
> @@ -812,7 +826,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
> @@ -825,6 +841,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;
> @@ -1065,6 +1082,50 @@ 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;
Should we allow the container itself to reset the failcount? All other
resource write handlers have a check that return EPERM when we are not
super ve. Here we will allow the container to reset its failcount so it
will no longer be trustworthy information.

-- 
Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH VZ10 v6 3/9] ve/fs: Rework per-ve mount count
  2026-08-19  9:07  3% ` [Devel] [PATCH VZ10 v6 3/9] ve/fs: Rework per-ve mount count Vladimir Riabchun
@ 2026-08-19 13:16  0%   ` Vasileios Almpanis
  0 siblings, 0 replies; 200+ results
From: Vasileios Almpanis @ 2026-08-19 13:16 UTC (permalink / raw)


> 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>
>
> diff --git a/fs/namespace.c b/fs/namespace.c
> index 68e0efb73d7c..c9e2ab9b3b57 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
> @@ -317,18 +317,21 @@ 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 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;
>  	}
>  
> @@ -336,6 +339,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;
> @@ -370,7 +381,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;
>  
> @@ -381,6 +393,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;
>  }
> @@ -750,7 +764,7 @@ 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);
> +	ve_mount_put(mnt, mnt->ve_owner);
this breaks compilation with CONFIG_VE=n. ve_onwer doesn't exist there.
Easiest solution would be to just wrap it under ifdef since the getting
stub also does nothing incase CONFIG_VE=n

-- 
Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH VZ10 v6 9/9] selftests/ve: Add mount accounting selftest
  2026-08-19  9:07  5% ` [Devel] [PATCH VZ10 v6 9/9] selftests/ve: Add mount accounting selftest Vladimir Riabchun
@ 2026-08-19 13:16  0%   ` Vasileios Almpanis
  0 siblings, 0 replies; 200+ results
From: Vasileios Almpanis @ 2026-08-19 13:16 UTC (permalink / raw)


> 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..a270a8cb8c51
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_mount_accounting_test.c
> @@ -0,0 +1,421 @@
> +// 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
> +
> +static int set_pseudosuper(int cgv2_fd, int ctid, int value)
> +{
> +	char path[64];
> +
> +	snprintf(path, sizeof(path), "%d/ve.pseudosuper", ctid);
> +	return write_u64_at(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 ret;
> +}
> +
> +static int create_mount(int cgv2_fd, int ctid, int id)
> +{
> +	int ret = run_in_ve(cgv2_fd, ctid, CLONE_NEWVE, _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(int cgv2_fd, int ctid, int id)
> +{
> +	int ret = run_in_ve(cgv2_fd, ctid, CLONE_NEWVE, _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(int cgv2_fd, int ctid)
> +{
> +	int avail1, avail2, mnt_id;
> +	char path[64];
> +
> +	mnt_id = get_free_mnt_id();
> +
> +	snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", ctid);
> +	if (mnt_id < 0 ||
> +	    read_s32_at(cgv2_fd, path, &avail1) ||
> +	    create_mount(cgv2_fd, ctid, mnt_id) ||
> +	    read_s32_at(cgv2_fd, path, &avail2) ||
> +	    destroy_mount(cgv2_fd, ctid, mnt_id))
> +		return -1;
> +
> +	return avail1 - avail2;
> +}
> +
> +/* Expect mount success and return new avail value */
> +static int mount_and_get_avail(struct __test_metadata *_metadata,
> +			int cgv2_fd, int ctid, int mnt_id)
> +{
> +	char path_avail[64];
> +	int mnt_avail_nr;
> +
> +	snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", ctid);
> +
> +	ASSERT_EQ(create_mount(cgv2_fd, ctid, mnt_id), 0);
> +	ASSERT_EQ(read_s32_at(cgv2_fd, path_avail, &mnt_avail_nr), 0);
> +	return mnt_avail_nr;
> +}
> +
> +/* Expect mount failure and ensure intact avail number */
> +static void assert_mount_fails(struct __test_metadata *_metadata,
> +			int cgv2_fd, int ctid, 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", ctid);
> +
> +	ASSERT_EQ(read_s32_at(cgv2_fd, path_avail, &mnt_avail_nr), 0);
> +	ASSERT_EQ(mnt_avail_nr, avail_count);
> +	ASSERT_LT(create_mount(cgv2_fd, ctid, mnt_id), 0);
> +	ASSERT_EQ(read_s32_at(cgv2_fd, path_avail, &mnt_avail_nr), 0);
> +	ASSERT_EQ(mnt_avail_nr, avail_count);
> +}
> +
> +FIXTURE(ve_mnt_acc)
> +{
> +	int cgv2_fd;
> +	int ctid;
> +};
> +
> +FIXTURE_SETUP(ve_mnt_acc)
> +{
> +	unsigned long long 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_u64_at(self->cgv2_fd, path, &initial_mnt_avail_nr), 0);
> +	ASSERT_EQ(initial_mnt_avail_nr, VE_MOUNTS_MAX);
> +};
> +
> +FIXTURE_TEARDOWN(ve_mnt_acc)
> +{
> +	for (int i = 0; i < MAX_MNT_ID; i++)
> +		_destroy_mount((void *)&i);
> +
> +	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->cgv2_fd, self->ctid, 0),
> +		  original_mnt_avail);
> +
> +	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 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 original_mnt_avail, mnt_avail_nr, mnt_cost;
> +	int original_have_mnt;
> +	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);
> +
> +	mnt_cost = get_mount_cost(self->cgv2_fd, self->ctid);
> +	ASSERT_GE(mnt_cost, 1);
> +
> +	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &original_mnt_avail), 0);
> +	original_have_mnt = VE_MOUNTS_MAX - original_mnt_avail;
> +
> +	/* Step 1: reduce number of available mounts to mnt_cost */
> +	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, original_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 2: do one mount, no mounts should be available */
> +	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 0),
> +		  0);
> +
> +	/* Step 3: check that one more mount falils */
NIT: fails
> +	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 1, 0);
> +
> +	/* Step 4: increase mount limit a little bit, mount should still fail */
> +	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
> +				original_have_mnt + 2 * mnt_cost - 1), 0);
> +	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 1, mnt_cost - 1);
> +
> +	/* Step 5: increase by 1 and win now */
> +	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, original_have_mnt + 2 * mnt_cost), 0);
> +	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
> +	ASSERT_EQ(mnt_avail_nr, mnt_cost);
> +
> +	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 1),
> +		  0);
> +
> +	/* Step 6: reduce mnt_max_nr so we have more mounts than allowed */
> +	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, original_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 7: try to do mount when avail < 0, ensure number is intact */
> +	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, -1 * mnt_cost);
> +
> +	/* Step 8: remove one mount, check avail value update, mount should fail */
> +	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 0), 0);
> +	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, 0);
> +
> +	/* Step 9: remove one more mount and check that new mount succeeds */
> +	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 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(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 2),
> +		  0);
> +
> +	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 2), 0);
> +}
> +
> +/*
> + * Mount propagation makes one mount cost more.
> + * This test check that if we run out or mounts in the middle of creating
NIT: This test checks.. out of mounts..

-- 
Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH vz10] selftests/damon: add script dir to sys.path for PYTHONSAFEPATH compatibility
       [not found]     ` <4492431c-87f8-4de6-9221-4e57107113bc@virtuozzo.com>
@ 2026-08-19 14:12  0%   ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-19 14:12 UTC (permalink / raw)


On 6/26/26 16:28, Pavel Tikhomirov wrote:
> On 6/26/26 13:34, Eva Kurchatova wrote:
>> The RHEL10 import (commit 9f055df11343) changed the Python shebang from
>> '#!/usr/bin/python3 -s' to '#!/usr/bin/python3 -sP'. The -P flag enables
>> Python's safe path mode (PYTHONSAFEPATH, added in Python 3.11), which
>> prevents prepending the script's directory to sys.path.
>>
>> This breaks all 7 DAMON Python selftests that import the _damon_sysfs
>> helper module located in the same directory:
>>
>>   ModuleNotFoundError: No module named '_damon_sysfs'
>>
>> Fix this by explicitly adding the script's directory to sys.path before
>> importing _damon_sysfs, following the same pattern used in commit
>> c3b3eb565bd7 ("tools: ynl: add script dir to sys.path") which fixed the
>> identical issue for the YNL tools.
>>
>> Fixes: 9f055df11343 ("rh10: import RHEL10 kernel-6.12.0-211.16.1.el10")
>> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
> 
> There is no -sP in mainstream so this test is not meant to be run with those
> flags, we should either avoid running this test or just remove the flags.

Well, i have to disagree here.
Yes, mainstream does not have -sP option at the moment,
at the same time they have already accepted the patch
  c3b3eb565bd7 ("tools: ynl: add script dir to sys.path")

which fixes for example tools/net/ynl/pyynl/cli.py (the files was moved later here)
which also lacks -sP python option, but still ther argument

>>>>>>>>
    Python options like PYTHONSAFEPATH or -P [1] do not add script
    directory to PYTHONPATH. ynl depends on this path to build and run.

    [1] This option is default for Fedora rpmbuild since introduction of
        https://fedoraproject.org/wiki/Changes/PythonSafePath
<<<<<<<<

worked fine. So i think that will be a direction in ms.

Currently RHEL10 kernel has ~120 files with "python -sP".

> 
>>
>> https://virtuozzo.atlassian.net/browse/VSTOR-132453
>> Feature: fix selftests
>>
>> ---
>>  tools/testing/selftests/damon/damon_nr_regions.py              | 3 +++
>>  tools/testing/selftests/damon/damos_apply_interval.py          | 3 +++
>>  tools/testing/selftests/damon/damos_quota.py                   | 3 +++
>>  tools/testing/selftests/damon/damos_quota_goal.py              | 3 +++
>>  tools/testing/selftests/damon/damos_tried_regions.py           | 3 +++
>>  .../selftests/damon/sysfs_update_schemes_tried_regions_hang.py | 3 +++
>>  .../damon/sysfs_update_schemes_tried_regions_wss_estimation.py | 3 +++
>>  7 files changed, 21 insertions(+)
>>
>> diff --git a/tools/testing/selftests/damon/damon_nr_regions.py b/tools/testing/selftests/damon/damon_nr_regions.py
>> index cb31cd140d22..8d0a352f7e85 100755
>> --- a/tools/testing/selftests/damon/damon_nr_regions.py
>> +++ b/tools/testing/selftests/damon/damon_nr_regions.py
>> @@ -1,9 +1,12 @@
>>  #! /usr/bin/python3 -sP
>>  # SPDX-License-Identifier: GPL-2.0
>>  
>> +import os
>>  import subprocess
>> +import sys
>>  import time
>>  
>> +sys.path.append(os.path.dirname(os.path.abspath(__file__)))
>>  import _damon_sysfs
>>  
>>  def test_nr_regions(real_nr_regions, min_nr_regions, max_nr_regions):
>> diff --git a/tools/testing/selftests/damon/damos_apply_interval.py b/tools/testing/selftests/damon/damos_apply_interval.py
>> index e81de3c06a8d..b1d78397bc41 100755
>> --- a/tools/testing/selftests/damon/damos_apply_interval.py
>> +++ b/tools/testing/selftests/damon/damos_apply_interval.py
>> @@ -1,9 +1,12 @@
>>  #! /usr/bin/python3 -sP
>>  # SPDX-License-Identifier: GPL-2.0
>>  
>> +import os
>>  import subprocess
>> +import sys
>>  import time
>>  
>> +sys.path.append(os.path.dirname(os.path.abspath(__file__)))
>>  import _damon_sysfs
>>  
>>  def main():
>> diff --git a/tools/testing/selftests/damon/damos_quota.py b/tools/testing/selftests/damon/damos_quota.py
>> index 7498b9472e9f..45b8b17f940a 100755
>> --- a/tools/testing/selftests/damon/damos_quota.py
>> +++ b/tools/testing/selftests/damon/damos_quota.py
>> @@ -1,9 +1,12 @@
>>  #! /usr/bin/python3 -sP
>>  # SPDX-License-Identifier: GPL-2.0
>>  
>> +import os
>>  import subprocess
>> +import sys
>>  import time
>>  
>> +sys.path.append(os.path.dirname(os.path.abspath(__file__)))
>>  import _damon_sysfs
>>  
>>  def main():
>> diff --git a/tools/testing/selftests/damon/damos_quota_goal.py b/tools/testing/selftests/damon/damos_quota_goal.py
>> index da43f69ed30d..a2d3abd780ae 100755
>> --- a/tools/testing/selftests/damon/damos_quota_goal.py
>> +++ b/tools/testing/selftests/damon/damos_quota_goal.py
>> @@ -1,9 +1,12 @@
>>  #! /usr/bin/python3 -sP
>>  # SPDX-License-Identifier: GPL-2.0
>>  
>> +import os
>>  import subprocess
>> +import sys
>>  import time
>>  
>> +sys.path.append(os.path.dirname(os.path.abspath(__file__)))
>>  import _damon_sysfs
>>  
>>  def main():
>> diff --git a/tools/testing/selftests/damon/damos_tried_regions.py b/tools/testing/selftests/damon/damos_tried_regions.py
>> index 7d1a44dbbe3d..188097475aa4 100755
>> --- a/tools/testing/selftests/damon/damos_tried_regions.py
>> +++ b/tools/testing/selftests/damon/damos_tried_regions.py
>> @@ -1,9 +1,12 @@
>>  #! /usr/bin/python3 -sP
>>  # SPDX-License-Identifier: GPL-2.0
>>  
>> +import os
>>  import subprocess
>> +import sys
>>  import time
>>  
>> +sys.path.append(os.path.dirname(os.path.abspath(__file__)))
>>  import _damon_sysfs
>>  
>>  def main():
>> diff --git a/tools/testing/selftests/damon/sysfs_update_schemes_tried_regions_hang.py b/tools/testing/selftests/damon/sysfs_update_schemes_tried_regions_hang.py
>> index 1ae639549f6d..b0710fb371f1 100755
>> --- a/tools/testing/selftests/damon/sysfs_update_schemes_tried_regions_hang.py
>> +++ b/tools/testing/selftests/damon/sysfs_update_schemes_tried_regions_hang.py
>> @@ -1,9 +1,12 @@
>>  #! /usr/bin/python3 -sP
>>  # SPDX-License-Identifier: GPL-2.0
>>  
>> +import os
>>  import subprocess
>> +import sys
>>  import time
>>  
>> +sys.path.append(os.path.dirname(os.path.abspath(__file__)))
>>  import _damon_sysfs
>>  
>>  def main():
>> diff --git a/tools/testing/selftests/damon/sysfs_update_schemes_tried_regions_wss_estimation.py b/tools/testing/selftests/damon/sysfs_update_schemes_tried_regions_wss_estimation.py
>> index b7d35ca4c129..cd8d901b8263 100755
>> --- a/tools/testing/selftests/damon/sysfs_update_schemes_tried_regions_wss_estimation.py
>> +++ b/tools/testing/selftests/damon/sysfs_update_schemes_tried_regions_wss_estimation.py
>> @@ -1,9 +1,12 @@
>>  #! /usr/bin/python3 -sP
>>  # SPDX-License-Identifier: GPL-2.0
>>  
>> +import os
>>  import subprocess
>> +import sys
>>  import time
>>  
>> +sys.path.append(os.path.dirname(os.path.abspath(__file__)))
>>  import _damon_sysfs
>>  
>>  def main():
> 


^ permalink raw reply	[relevance 0%]

* [Devel] [PATCH RHEL10 COMMIT] selftests/damon: add script dir to sys.path for PYTHONSAFEPATH compatibility
       [not found]     <20260626113435.2210877-1-eva.kurchatova@virtuozzo.com>
       [not found]     ` <4492431c-87f8-4de6-9221-4e57107113bc@virtuozzo.com>
@ 2026-08-19 14:16  3% ` Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-19 14:16 UTC (permalink / raw)


The commit is pushed to "branch-rh10-6.12.0-211.39.1.16.x.vz10-ovz" and will appear at git at bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.39.1.16.6.vz10
------>
commit e6c430f5a2cc31a0244492728ab9af8d0530710a
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
Date:   Fri Jun 26 14:34:27 2026 +0300

    selftests/damon: add script dir to sys.path for PYTHONSAFEPATH compatibility
    
    The RHEL10 import (commit 9f055df11343) changed the Python shebang from
    '#!/usr/bin/python3 -s' to '#!/usr/bin/python3 -sP'. The -P flag enables
    Python's safe path mode (PYTHONSAFEPATH, added in Python 3.11), which
    prevents prepending the script's directory to sys.path.
    
    This breaks all 7 DAMON Python selftests that import the _damon_sysfs
    helper module located in the same directory:
    
      ModuleNotFoundError: No module named '_damon_sysfs'
    
    Fix this by explicitly adding the script's directory to sys.path before
    importing _damon_sysfs, following the same pattern used in commit
    c3b3eb565bd7 ("tools: ynl: add script dir to sys.path") which fixed the
    identical issue for the YNL tools.
    
    Note: CentOS 10 Stream git does NOT have "-sP" due to some reason [1].
    
    [1] https://gitlab.com/redhat/centos-stream/src/kernel/centos-stream-10/-/blob/main/tools/testing/selftests/damon/damon_nr_regions.py
    
    Fixes: 9f055df11343 ("rh10: import RHEL10 kernel-6.12.0-211.16.1.el10")
    https://virtuozzo.atlassian.net/browse/VSTOR-132453
    Feature: fix selftests
    Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
    Acked-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 tools/testing/selftests/damon/damon_nr_regions.py                      | 3 +++
 tools/testing/selftests/damon/damos_apply_interval.py                  | 3 +++
 tools/testing/selftests/damon/damos_quota.py                           | 3 +++
 tools/testing/selftests/damon/damos_quota_goal.py                      | 3 +++
 tools/testing/selftests/damon/damos_tried_regions.py                   | 3 +++
 .../testing/selftests/damon/sysfs_update_schemes_tried_regions_hang.py | 3 +++
 .../damon/sysfs_update_schemes_tried_regions_wss_estimation.py         | 3 +++
 7 files changed, 21 insertions(+)

diff --git a/tools/testing/selftests/damon/damon_nr_regions.py b/tools/testing/selftests/damon/damon_nr_regions.py
index cb31cd140d220..8d0a352f7e85c 100755
--- a/tools/testing/selftests/damon/damon_nr_regions.py
+++ b/tools/testing/selftests/damon/damon_nr_regions.py
@@ -1,9 +1,12 @@
 #! /usr/bin/python3 -sP
 # SPDX-License-Identifier: GPL-2.0
 
+import os
 import subprocess
+import sys
 import time
 
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
 import _damon_sysfs
 
 def test_nr_regions(real_nr_regions, min_nr_regions, max_nr_regions):
diff --git a/tools/testing/selftests/damon/damos_apply_interval.py b/tools/testing/selftests/damon/damos_apply_interval.py
index e81de3c06a8d2..b1d78397bc41d 100755
--- a/tools/testing/selftests/damon/damos_apply_interval.py
+++ b/tools/testing/selftests/damon/damos_apply_interval.py
@@ -1,9 +1,12 @@
 #! /usr/bin/python3 -sP
 # SPDX-License-Identifier: GPL-2.0
 
+import os
 import subprocess
+import sys
 import time
 
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
 import _damon_sysfs
 
 def main():
diff --git a/tools/testing/selftests/damon/damos_quota.py b/tools/testing/selftests/damon/damos_quota.py
index 7498b9472e9f7..45b8b17f940a0 100755
--- a/tools/testing/selftests/damon/damos_quota.py
+++ b/tools/testing/selftests/damon/damos_quota.py
@@ -1,9 +1,12 @@
 #! /usr/bin/python3 -sP
 # SPDX-License-Identifier: GPL-2.0
 
+import os
 import subprocess
+import sys
 import time
 
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
 import _damon_sysfs
 
 def main():
diff --git a/tools/testing/selftests/damon/damos_quota_goal.py b/tools/testing/selftests/damon/damos_quota_goal.py
index da43f69ed30db..a2d3abd780aeb 100755
--- a/tools/testing/selftests/damon/damos_quota_goal.py
+++ b/tools/testing/selftests/damon/damos_quota_goal.py
@@ -1,9 +1,12 @@
 #! /usr/bin/python3 -sP
 # SPDX-License-Identifier: GPL-2.0
 
+import os
 import subprocess
+import sys
 import time
 
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
 import _damon_sysfs
 
 def main():
diff --git a/tools/testing/selftests/damon/damos_tried_regions.py b/tools/testing/selftests/damon/damos_tried_regions.py
index 7d1a44dbbe3df..188097475aa41 100755
--- a/tools/testing/selftests/damon/damos_tried_regions.py
+++ b/tools/testing/selftests/damon/damos_tried_regions.py
@@ -1,9 +1,12 @@
 #! /usr/bin/python3 -sP
 # SPDX-License-Identifier: GPL-2.0
 
+import os
 import subprocess
+import sys
 import time
 
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
 import _damon_sysfs
 
 def main():
diff --git a/tools/testing/selftests/damon/sysfs_update_schemes_tried_regions_hang.py b/tools/testing/selftests/damon/sysfs_update_schemes_tried_regions_hang.py
index 1ae639549f6de..b0710fb371f14 100755
--- a/tools/testing/selftests/damon/sysfs_update_schemes_tried_regions_hang.py
+++ b/tools/testing/selftests/damon/sysfs_update_schemes_tried_regions_hang.py
@@ -1,9 +1,12 @@
 #! /usr/bin/python3 -sP
 # SPDX-License-Identifier: GPL-2.0
 
+import os
 import subprocess
+import sys
 import time
 
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
 import _damon_sysfs
 
 def main():
diff --git a/tools/testing/selftests/damon/sysfs_update_schemes_tried_regions_wss_estimation.py b/tools/testing/selftests/damon/sysfs_update_schemes_tried_regions_wss_estimation.py
index b7d35ca4c129a..cd8d901b8263b 100755
--- a/tools/testing/selftests/damon/sysfs_update_schemes_tried_regions_wss_estimation.py
+++ b/tools/testing/selftests/damon/sysfs_update_schemes_tried_regions_wss_estimation.py
@@ -1,9 +1,12 @@
 #! /usr/bin/python3 -sP
 # SPDX-License-Identifier: GPL-2.0
 
+import os
 import subprocess
+import sys
 import time
 
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
 import _damon_sysfs
 
 def main():

^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH vz10 v2] ve: downgrade the trusted exec/mmap denial from WARN to pr_warn
       [not found]     <20260706110002.1024515-9-khorenko@virtuozzo.com>
@ 2026-08-19 16:25 13% ` Konstantin Khorenko
  2026-08-26 16:49  0%   ` Konstantin Khorenko
  0 siblings, 1 reply; 200+ results
From: Konstantin Khorenko @ 2026-08-19 16:25 UTC (permalink / raw)


ve_check_trusted_exec()/ve_check_trusted_mmap() protect the host, but
the WARN(1, ...) is too much here:

 * it taints the kernel while this is not a kernel bug which we beed to
   debug and fix
 * the backtrace only shows the exec/mmap path, which is already known
 * on a host booted with panic_on_warn a denied exec takes the host down

The SIGSEGV and the core dump of the offending process are enough to
report and investigate such an attempt. Downgrade the WARN() to
pr_warn(), keeping the message, the rate limiting and the signal.

Fixes: fc7157b84c32 ("trusted/ve/mmap: Protect from unsecure library load from CT image")
Fixes: cc218b70c6b4 ("trusted/ve/fs/exec: Send SIGSEGV to a process trying to execute untrusted files")
Feature: security/fs: tructed exec feature
https://virtuozzo.atlassian.net/browse/VSTOR-137234
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
v1 -> v2: commit message rewritten
---
 kernel/ve/ve.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index 73d1c3b4873e5..0d43dc3253ea3 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -1745,8 +1745,8 @@ bool ve_check_trusted_mmap(struct file *file)
 	if (file->f_path.dentry)
 		filename = file->f_path.dentry->d_name.name;
 
-	WARN(1, "VE0 %s tried to map code from file '%s' from VEX\n",
-			current->comm, filename);
+	pr_warn("VE0 %s tried to map code from file '%s' from VEX\n",
+		current->comm, filename);
 	force_sigsegv(SIGSEGV);
 	return false;
 }
@@ -1765,7 +1765,7 @@ bool ve_check_trusted_exec(struct file *file, struct filename *name)
 	if (!__ratelimit(&sigsegv_rs))
 		return false;
 
-	WARN(1, "VE0's %s tried to execute untrusted file %s from VEX\n",
+	pr_warn("VE0's %s tried to execute untrusted file %s from VEX\n",
 		current->comm, name->name);
 	force_sigsegv(SIGSEGV);
 	return false;
-- 
2.43.0


^ permalink raw reply	[relevance 13%]

* [Devel] [PATCH vz10] selftests/damon: wait for the merge to apply the new max_nr_regions
@ 2026-08-21 15:13 13% Eva Kurchatova
  2026-08-31 23:22  0% ` Eva Kurchatova (Virtuozzo)
  0 siblings, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-08-21 15:13 UTC (permalink / raw)


damon_nr_regions sleeps 0.3s after committing max_nr_regions and then
reads the number of regions back, which assumes the merge has happened
by then.  On a machine that is not idle it has not, and the test fails
on a region count that is still the old one.

Poll until the count settles instead of sleeping for a fixed time.

https://virtuozzo.atlassian.net/browse/VSTOR-132453
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 .../selftests/damon/damon_nr_regions.py       | 35 +++++++++++--------
 1 file changed, 20 insertions(+), 15 deletions(-)

diff --git a/tools/testing/selftests/damon/damon_nr_regions.py b/tools/testing/selftests/damon/damon_nr_regions.py
index cb31cd140d22..39063ce5b27d 100755
--- a/tools/testing/selftests/damon/damon_nr_regions.py
+++ b/tools/testing/selftests/damon/damon_nr_regions.py
@@ -4,6 +4,7 @@
 import subprocess
 import time
 
+
 import _damon_sysfs
 
 def test_nr_regions(real_nr_regions, min_nr_regions, max_nr_regions):
@@ -114,27 +115,31 @@ def main():
         proc.terminate()
         print('commit failed: %s' % err)
         exit(1)
-    # wait for next merge operation is executed
-    time.sleep(0.3)
+    # wait for the merge operations to apply the new max_nr_regions
+    nr_tried_regions = 0
+    for _ in range(50):
+        time.sleep(0.1)
 
-    err = kdamonds.kdamonds[0].update_schemes_tried_regions()
-    if err is not None:
-        proc.terminate()
-        print('tried regions update failed: %s' % err)
-        exit(1)
+        err = kdamonds.kdamonds[0].update_schemes_tried_regions()
+        if err is not None:
+            proc.terminate()
+            print('tried regions update failed: %s' % err)
+            exit(1)
 
-    scheme = kdamonds.kdamonds[0].contexts[0].schemes[0]
-    if scheme.tried_regions is None:
-        proc.terminate()
-        print('tried regions is not collected')
-        exit(1)
+        scheme = kdamonds.kdamonds[0].contexts[0].schemes[0]
+        if scheme.tried_regions is None:
+            proc.terminate()
+            print('tried regions is not collected')
+            exit(1)
+
+        nr_tried_regions = len(scheme.tried_regions)
+        if 0 < nr_tried_regions <= 7:
+            break
+    proc.terminate()
 
-    nr_tried_regions = len(scheme.tried_regions)
     if nr_tried_regions <= 0:
-        proc.terminate()
         print('tried regions is not created')
         exit(1)
-    proc.terminate()
 
     if nr_tried_regions > 7:
         print('fail online-tuned max_nr_regions: %d > 7' % nr_tried_regions)
-- 
2.55.0


^ permalink raw reply	[relevance 13%]

* [Devel] [PATCH vz10 10/32] mm, proc: build the /proc/meminfo virtualization only with CONFIG_VE
  @ 2026-08-21 16:36  5% ` Konstantin Khorenko
  2026-08-21 16:42  5%   ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
  2026-08-21 16:37  9% ` [Devel] [PATCH vz10 29/32] ms/pcmcia: cistpl: Constify 'struct bin_attribute' Konstantin Khorenko
  2026-08-21 16:37  4% ` [Devel] [PATCH vz10 32/32] kunit: add the script dir to sys.path for PYTHONSAFEPATH compatibility Konstantin Khorenko
  2 siblings, 1 reply; 200+ results
From: Konstantin Khorenko @ 2026-08-21 16:36 UTC (permalink / raw)


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

The Container view of /proc/meminfo is built from the memory cgroup of
the Container: si_meminfo_ve() reads memcg->memory / memcg->memsw and
fill_meminfo_ve() looks the cgroup up by memory_cgrp_id.  Both are
compiled unconditionally, so with CONFIG_MEMCG=n - which CONFIG_VE=n
allows, and plain "make defconfig" used to produce - the build fails:

  mm/show_mem.c: error: 'memory_cgrp_id' undeclared (first use in this
                 function)

CONFIG_VE selects CONFIG_MEMCG, so CONFIG_VE is the condition to guard
with, and it is the better one anyway: with CONFIG_VE=n the Container view
is dead code even where CONFIG_MEMCG is on.  Compile si_meminfo_ve(),
fill_meminfo_ve() and the code that formats that view under it.

meminfo_proc_show_mi() has to go under the same guard as its only caller,
or it is left as an unused static function - an error rather than a
warning in a CONFIG_VE=n build with CONFIG_WERROR=y, which x86_64
defconfig sets.

The callers of si_meminfo_ve() in do_sysinfo() and swaps_open() are
reached only when !ve_is_super(get_exec_env()), which is a compile-time
false with CONFIG_VE=n, so the compiler drops those branches and no stub
is needed.

Fixes: 7710faa9d4f0 ("ve/proc: virtualize /proc/meminfo in a Container")
Feature: procfs: virtualize /proc/meminfo
https://virtuozzo.atlassian.net/browse/VSTOR-134732
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 fs/proc/meminfo.c | 4 ++++
 mm/show_mem.c     | 2 ++
 2 files changed, 6 insertions(+)

diff --git a/fs/proc/meminfo.c b/fs/proc/meminfo.c
index ac3c88e68728..27e8f921f31a 100644
--- a/fs/proc/meminfo.c
+++ b/fs/proc/meminfo.c
@@ -34,6 +34,7 @@ static void show_val_kb(struct seq_file *m, const char *s, unsigned long num)
 	seq_write(m, " kB\n", 4);
 }
 
+#ifdef CONFIG_VE
 static int meminfo_proc_show_mi(struct seq_file *m, struct meminfo *mi)
 {
 	unsigned long *pages;
@@ -84,6 +85,7 @@ static void fill_meminfo_ve(struct meminfo *mi, struct ve_struct *ve)
 	css_put(css);
 
 }
+#endif /* CONFIG_VE */
 
 static int meminfo_proc_show_ve(struct seq_file *m, void *v,
 				struct ve_struct *ve)
@@ -104,11 +106,13 @@ static int meminfo_proc_show_ve(struct seq_file *m, void *v,
         mi.si = &i;
         mi.ve = ve;
 
+#ifdef CONFIG_VE
 	if (!ve_is_super(ve) && ve->meminfo_val == VE_MEMINFO_DEFAULT) {
 		fill_meminfo_ve(&mi, ve);
 
 		return meminfo_proc_show_mi(m, &mi);
 	}
+#endif
 
 	committed = vm_memory_committed();
 
diff --git a/mm/show_mem.c b/mm/show_mem.c
index 3ab11c945bf4..15be6c6050e4 100644
--- a/mm/show_mem.c
+++ b/mm/show_mem.c
@@ -91,6 +91,7 @@ void si_meminfo(struct sysinfo *val)
 
 EXPORT_SYMBOL(si_meminfo);
 
+#ifdef CONFIG_VE
 void si_meminfo_ve(struct sysinfo *si, struct ve_struct *ve)
 {
 	unsigned long memtotal, memused, swaptotal, swapused;
@@ -138,6 +139,7 @@ void si_meminfo_ve(struct sysinfo *si, struct ve_struct *ve)
 	/* bufferram, totalhigh and freehigh left 0 */
 }
 EXPORT_SYMBOL(si_meminfo_ve);
+#endif /* CONFIG_VE */
 
 #ifdef CONFIG_NUMA
 void si_meminfo_node(struct sysinfo *val, int nid)
-- 
2.47.1


^ permalink raw reply	[relevance 5%]

* [Devel] [PATCH vz10 29/32] ms/pcmcia: cistpl: Constify 'struct bin_attribute'
    2026-08-21 16:36  5% ` [Devel] [PATCH vz10 10/32] mm, proc: build the /proc/meminfo virtualization only with CONFIG_VE Konstantin Khorenko
@ 2026-08-21 16:37  9% ` Konstantin Khorenko
  2026-08-21 16:42  9%   ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
  2026-08-21 16:37  4% ` [Devel] [PATCH vz10 32/32] kunit: add the script dir to sys.path for PYTHONSAFEPATH compatibility Konstantin Khorenko
  2 siblings, 1 reply; 200+ results
From: Konstantin Khorenko @ 2026-08-21 16:37 UTC (permalink / raw)


From: Thomas Wei?schuh <linux@weissschuh.net>

The sysfs core now allows instances of 'struct bin_attribute' to be
moved into read-only memory. Make use of that to protect them against
accidental or malicious modifications.

Signed-off-by: Thomas Wei?schuh <linux@weissschuh.net>
Link: https://lore.kernel.org/r/20241215-sysfs-const-bin_attr-pcmcia-v1-1-ebb82e47d834@weissschuh.net
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
(cherry picked from commit 05a9896fa9e15466456a1b1dc9d2eacdf3551b79)

Needed here because the RHEL10.2 base carries the sysfs side of the
conversion but not the pcmcia one, so the initializer of pccard_cis_attr
does not compile at all:

  drivers/pcmcia/cistpl.c:1608:17: error: initialization of
    'ssize_t (*)(struct file *, struct kobject *,
                 const struct bin_attribute *, char *, loff_t, size_t)'
    from incompatible pointer type [-Wincompatible-pointer-types]

Our shipped configs have CONFIG_PCCARD=n so it goes unnoticed there,
while plain x86_64 defconfig - which we now want to keep building for
KUnit - enables it.  Applies as is: this tree's struct bin_attribute has
both the ::read/::write and the ::read_new/::write_new members, and
sysfs_kf_bin_read() prefers the latter.

https://virtuozzo.atlassian.net/browse/VSTOR-134732
Feature: fix ms/pcmcia
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 drivers/pcmcia/cistpl.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/drivers/pcmcia/cistpl.c b/drivers/pcmcia/cistpl.c
index d018f36f3a89..0c801e4ccc6c 100644
--- a/drivers/pcmcia/cistpl.c
+++ b/drivers/pcmcia/cistpl.c
@@ -1540,7 +1540,7 @@ static ssize_t pccard_extract_cis(struct pcmcia_socket *s, char *buf,
 
 
 static ssize_t pccard_show_cis(struct file *filp, struct kobject *kobj,
-			       struct bin_attribute *bin_attr,
+			       const struct bin_attribute *bin_attr,
 			       char *buf, loff_t off, size_t count)
 {
 	unsigned int size = 0x200;
@@ -1571,7 +1571,7 @@ static ssize_t pccard_show_cis(struct file *filp, struct kobject *kobj,
 
 
 static ssize_t pccard_store_cis(struct file *filp, struct kobject *kobj,
-				struct bin_attribute *bin_attr,
+				const struct bin_attribute *bin_attr,
 				char *buf, loff_t off, size_t count)
 {
 	struct pcmcia_socket *s;
@@ -1605,6 +1605,6 @@ static ssize_t pccard_store_cis(struct file *filp, struct kobject *kobj,
 const struct bin_attribute pccard_cis_attr = {
 	.attr = { .name = "cis", .mode = S_IRUGO | S_IWUSR },
 	.size = 0x200,
-	.read = pccard_show_cis,
-	.write = pccard_store_cis,
+	.read_new = pccard_show_cis,
+	.write_new = pccard_store_cis,
 };
-- 
2.47.1


^ permalink raw reply	[relevance 9%]

* [Devel] [PATCH vz10 32/32] kunit: add the script dir to sys.path for PYTHONSAFEPATH compatibility
    2026-08-21 16:36  5% ` [Devel] [PATCH vz10 10/32] mm, proc: build the /proc/meminfo virtualization only with CONFIG_VE Konstantin Khorenko
  2026-08-21 16:37  9% ` [Devel] [PATCH vz10 29/32] ms/pcmcia: cistpl: Constify 'struct bin_attribute' Konstantin Khorenko
@ 2026-08-21 16:37  4% ` Konstantin Khorenko
  2026-08-21 16:42  3%   ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
  2 siblings, 1 reply; 200+ results
From: Konstantin Khorenko @ 2026-08-21 16:37 UTC (permalink / raw)


The RHEL10 import changed the Python shebang of the in-tree tools from
'#!/usr/bin/python3 -s' to '#!/usr/bin/python3 -sP'.  The -P flag turns on
safe path mode (PYTHONSAFEPATH, Python 3.11+), which stops Python from
prepending the script's own directory to sys.path - so kunit.py cannot
import the modules sitting right next to it, and the KUnit tool does not
start at all:

  $ ./tools/testing/kunit/kunit.py run
  Traceback (most recent call last):
    File "tools/testing/kunit/kunit.py", line 23, in <module>
      import kunit_json
  ModuleNotFoundError: No module named 'kunit_json'

Add the script's directory to sys.path before the local imports in the two
executable scripts that have them, kunit.py and kunit_tool_test.py,
following commit e6c430f5a2cc ("selftests/damon: add script dir to
sys.path for PYTHONSAFEPATH compatibility") which fixed the same breakage
for the DAMON selftests.

Fixes: 9f055df11343 ("rh10: import RHEL10 kernel-6.12.0-211.16.1.el10")
Feature: fix KUnit tests
https://virtuozzo.atlassian.net/browse/VSTOR-134732
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 tools/testing/kunit/kunit.py           | 1 +
 tools/testing/kunit/kunit_tool_test.py | 2 ++
 2 files changed, 3 insertions(+)

diff --git a/tools/testing/kunit/kunit.py b/tools/testing/kunit/kunit.py
index 13ccb9993776..b4bc6c742718 100755
--- a/tools/testing/kunit/kunit.py
+++ b/tools/testing/kunit/kunit.py
@@ -20,6 +20,7 @@ from dataclasses import dataclass
 from enum import Enum, auto
 from typing import Iterable, List, Optional, Sequence, Tuple
 
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
 import kunit_json
 import kunit_kernel
 import kunit_parser
diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py
index 3121e133e949..265a8999aada 100755
--- a/tools/testing/kunit/kunit_tool_test.py
+++ b/tools/testing/kunit/kunit_tool_test.py
@@ -16,8 +16,10 @@ import json
 import os
 import signal
 import subprocess
+import sys
 from typing import Iterable
 
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
 import kunit_config
 import kunit_parser
 import kunit_kernel
-- 
2.47.1


^ permalink raw reply	[relevance 4%]

* [Devel] [PATCH vz10 1/3] selftests: net: Adapt ethtool mq tests to fix in qdisc graft
@ 2026-08-21 16:41  4% Eva Kurchatova
  2026-08-24 13:36  0% ` Konstantin Khorenko
  2026-08-24 14:36  3% ` [Devel] [PATCH RHEL10 COMMIT] ms/selftests: " Konstantin Khorenko
  0 siblings, 2 replies; 200+ results
From: Eva Kurchatova @ 2026-08-21 16:41 UTC (permalink / raw)


From: Victor Nogueira <victor@mojatatu.com>

Because of patch[1] the graft behaviour changed

So the command:

tcq replace parent 100:1 handle 204:

Is no longer valid and will not delete 100:4 added by command:

tcq replace parent 100:4 handle 204: pfifo_fast

So to maintain the original behaviour, this patch manually deletes 100:4
and grafts 100:1

Note: This change will also work fine without [1]

[1] https://lore.kernel.org/netdev/20250111151455.75480-1-jhs at mojatatu.com/T/#u

Signed-off-by: Victor Nogueira <victor@mojatatu.com>
Reviewed-by: Jamal Hadi Salim <jhs@mojatatu.com>
Signed-off-by: David S. Miller <davem@davemloft.net>
(cherry picked from commit 0a5b8fff01bde1b9908f00004c676f2e2459333b)

https://virtuozzo.atlassian.net/browse/VSTOR-139651
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 .../selftests/drivers/net/netdevsim/tc-mq-visibility.sh  | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/tools/testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh b/tools/testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh
index fd13c8cfb7a8..b411fe66510f 100755
--- a/tools/testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh
+++ b/tools/testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh
@@ -58,9 +58,12 @@ for root in mq mqprio; do
     ethtool -L $NDEV combined 4
     n_child_assert 4 "One real queue, rest default"
 
-    # Graft some
-    tcq replace parent 100:1 handle 204:
-    n_child_assert 3 "Grafted"
+    # Remove real one
+    tcq del parent 100:4 handle 204:
+
+    # Replace default with pfifo
+    tcq replace parent 100:1 handle 205: pfifo limit 1000
+    n_child_assert 3 "Deleting real one, replacing default one with pfifo"
 
     ethtool -L $NDEV combined 1
     n_child_assert 1 "Grafted, one"
-- 
2.55.0


^ permalink raw reply	[relevance 4%]

* [Devel] [PATCH RHEL10 COMMIT] mm, proc: build the /proc/meminfo virtualization only with CONFIG_VE
  2026-08-21 16:36  5% ` [Devel] [PATCH vz10 10/32] mm, proc: build the /proc/meminfo virtualization only with CONFIG_VE Konstantin Khorenko
@ 2026-08-21 16:42  5%   ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-21 16:42 UTC (permalink / raw)


The commit is pushed to "branch-rh10-6.12.0-211.39.1.16.x.vz10-ovz" and will appear at git at bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.39.1.16.6.vz10
------>
commit 3bb167409cb19772fcb7e26a2a51afd77ef4dd1c
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
Date:   Fri Aug 21 18:36:56 2026 +0200

    mm, proc: build the /proc/meminfo virtualization only with CONFIG_VE
    
    The Container view of /proc/meminfo is built from the memory cgroup of
    the Container: si_meminfo_ve() reads memcg->memory / memcg->memsw and
    fill_meminfo_ve() looks the cgroup up by memory_cgrp_id.  Both are
    compiled unconditionally, so with CONFIG_MEMCG=n - which CONFIG_VE=n
    allows, and plain "make defconfig" used to produce - the build fails:
    
      mm/show_mem.c: error: 'memory_cgrp_id' undeclared (first use in this
                     function)
    
    CONFIG_VE selects CONFIG_MEMCG, so CONFIG_VE is the condition to guard
    with, and it is the better one anyway: with CONFIG_VE=n the Container view
    is dead code even where CONFIG_MEMCG is on.  Compile si_meminfo_ve(),
    fill_meminfo_ve() and the code that formats that view under it.
    
    meminfo_proc_show_mi() has to go under the same guard as its only caller,
    or it is left as an unused static function - an error rather than a
    warning in a CONFIG_VE=n build with CONFIG_WERROR=y, which x86_64
    defconfig sets.
    
    The callers of si_meminfo_ve() in do_sysinfo() and swaps_open() are
    reached only when !ve_is_super(get_exec_env()), which is a compile-time
    false with CONFIG_VE=n, so the compiler drops those branches and no stub
    is needed.
    
    Fixes: 7710faa9d4f0 ("ve/proc: virtualize /proc/meminfo in a Container")
    Feature: procfs: virtualize /proc/meminfo
    https://virtuozzo.atlassian.net/browse/VSTOR-134732
    Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
    
    Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 fs/proc/meminfo.c | 4 ++++
 mm/show_mem.c     | 2 ++
 2 files changed, 6 insertions(+)

diff --git a/fs/proc/meminfo.c b/fs/proc/meminfo.c
index ac3c88e68728a..27e8f921f31af 100644
--- a/fs/proc/meminfo.c
+++ b/fs/proc/meminfo.c
@@ -34,6 +34,7 @@ static void show_val_kb(struct seq_file *m, const char *s, unsigned long num)
 	seq_write(m, " kB\n", 4);
 }
 
+#ifdef CONFIG_VE
 static int meminfo_proc_show_mi(struct seq_file *m, struct meminfo *mi)
 {
 	unsigned long *pages;
@@ -84,6 +85,7 @@ static void fill_meminfo_ve(struct meminfo *mi, struct ve_struct *ve)
 	css_put(css);
 
 }
+#endif /* CONFIG_VE */
 
 static int meminfo_proc_show_ve(struct seq_file *m, void *v,
 				struct ve_struct *ve)
@@ -104,11 +106,13 @@ static int meminfo_proc_show_ve(struct seq_file *m, void *v,
         mi.si = &i;
         mi.ve = ve;
 
+#ifdef CONFIG_VE
 	if (!ve_is_super(ve) && ve->meminfo_val == VE_MEMINFO_DEFAULT) {
 		fill_meminfo_ve(&mi, ve);
 
 		return meminfo_proc_show_mi(m, &mi);
 	}
+#endif
 
 	committed = vm_memory_committed();
 
diff --git a/mm/show_mem.c b/mm/show_mem.c
index 3ab11c945bf4a..15be6c6050e4d 100644
--- a/mm/show_mem.c
+++ b/mm/show_mem.c
@@ -91,6 +91,7 @@ void si_meminfo(struct sysinfo *val)
 
 EXPORT_SYMBOL(si_meminfo);
 
+#ifdef CONFIG_VE
 void si_meminfo_ve(struct sysinfo *si, struct ve_struct *ve)
 {
 	unsigned long memtotal, memused, swaptotal, swapused;
@@ -138,6 +139,7 @@ void si_meminfo_ve(struct sysinfo *si, struct ve_struct *ve)
 	/* bufferram, totalhigh and freehigh left 0 */
 }
 EXPORT_SYMBOL(si_meminfo_ve);
+#endif /* CONFIG_VE */
 
 #ifdef CONFIG_NUMA
 void si_meminfo_node(struct sysinfo *val, int nid)

^ permalink raw reply	[relevance 5%]

* [Devel] [PATCH RHEL10 COMMIT] ms/pcmcia: cistpl: Constify 'struct bin_attribute'
  2026-08-21 16:37  9% ` [Devel] [PATCH vz10 29/32] ms/pcmcia: cistpl: Constify 'struct bin_attribute' Konstantin Khorenko
@ 2026-08-21 16:42  9%   ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-21 16:42 UTC (permalink / raw)


The commit is pushed to "branch-rh10-6.12.0-211.39.1.16.x.vz10-ovz" and will appear at git at bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.39.1.16.6.vz10
------>
commit 889c35dad8118bbce4445b873c67a6d7a931db22
Author: Thomas Wei??schuh <linux@weissschuh.net>
Date:   Fri Aug 21 18:37:15 2026 +0200

    ms/pcmcia: cistpl: Constify 'struct bin_attribute'
    
    The sysfs core now allows instances of 'struct bin_attribute' to be
    moved into read-only memory. Make use of that to protect them against
    accidental or malicious modifications.
    
    Signed-off-by: Thomas Wei??schuh <linux@weissschuh.net>
    Link: https://lore.kernel.org/r/20241215-sysfs-const-bin_attr-pcmcia-v1-1-ebb82e47d834 at weissschuh.net
    Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
    (cherry picked from commit 05a9896fa9e15466456a1b1dc9d2eacdf3551b79)
    
    Needed here because the RHEL10.2 base carries the sysfs side of the
    conversion but not the pcmcia one, so the initializer of pccard_cis_attr
    does not compile at all:
    
      drivers/pcmcia/cistpl.c:1608:17: error: initialization of
        'ssize_t (*)(struct file *, struct kobject *,
                     const struct bin_attribute *, char *, loff_t, size_t)'
        from incompatible pointer type [-Wincompatible-pointer-types]
    
    Our shipped configs have CONFIG_PCCARD=n so it goes unnoticed there,
    while plain x86_64 defconfig - which we now want to keep building for
    KUnit - enables it.  Applies as is: this tree's struct bin_attribute has
    both the ::read/::write and the ::read_new/::write_new members, and
    sysfs_kf_bin_read() prefers the latter.
    
    https://virtuozzo.atlassian.net/browse/VSTOR-134732
    Feature: fix ms/pcmcia
    Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 drivers/pcmcia/cistpl.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/drivers/pcmcia/cistpl.c b/drivers/pcmcia/cistpl.c
index d018f36f3a893..0c801e4ccc6c2 100644
--- a/drivers/pcmcia/cistpl.c
+++ b/drivers/pcmcia/cistpl.c
@@ -1540,7 +1540,7 @@ static ssize_t pccard_extract_cis(struct pcmcia_socket *s, char *buf,
 
 
 static ssize_t pccard_show_cis(struct file *filp, struct kobject *kobj,
-			       struct bin_attribute *bin_attr,
+			       const struct bin_attribute *bin_attr,
 			       char *buf, loff_t off, size_t count)
 {
 	unsigned int size = 0x200;
@@ -1571,7 +1571,7 @@ static ssize_t pccard_show_cis(struct file *filp, struct kobject *kobj,
 
 
 static ssize_t pccard_store_cis(struct file *filp, struct kobject *kobj,
-				struct bin_attribute *bin_attr,
+				const struct bin_attribute *bin_attr,
 				char *buf, loff_t off, size_t count)
 {
 	struct pcmcia_socket *s;
@@ -1605,6 +1605,6 @@ static ssize_t pccard_store_cis(struct file *filp, struct kobject *kobj,
 const struct bin_attribute pccard_cis_attr = {
 	.attr = { .name = "cis", .mode = S_IRUGO | S_IWUSR },
 	.size = 0x200,
-	.read = pccard_show_cis,
-	.write = pccard_store_cis,
+	.read_new = pccard_show_cis,
+	.write_new = pccard_store_cis,
 };

^ permalink raw reply	[relevance 9%]

* [Devel] [PATCH RHEL10 COMMIT] kunit: add the script dir to sys.path for PYTHONSAFEPATH compatibility
  2026-08-21 16:37  4% ` [Devel] [PATCH vz10 32/32] kunit: add the script dir to sys.path for PYTHONSAFEPATH compatibility Konstantin Khorenko
@ 2026-08-21 16:42  3%   ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-21 16:42 UTC (permalink / raw)


The commit is pushed to "branch-rh10-6.12.0-211.39.1.16.x.vz10-ovz" and will appear at git at bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.39.1.16.6.vz10
------>
commit 7a2f379b54dc30a92d1f758221cddbb78f7c1f74
Author: Konstantin Khorenko <khorenko@virtuozzo.com>
Date:   Fri Aug 21 18:37:18 2026 +0200

    kunit: add the script dir to sys.path for PYTHONSAFEPATH compatibility
    
    The RHEL10 import changed the Python shebang of the in-tree tools from
    '#!/usr/bin/python3 -s' to '#!/usr/bin/python3 -sP'.  The -P flag turns on
    safe path mode (PYTHONSAFEPATH, Python 3.11+), which stops Python from
    prepending the script's own directory to sys.path - so kunit.py cannot
    import the modules sitting right next to it, and the KUnit tool does not
    start at all:
    
      $ ./tools/testing/kunit/kunit.py run
      Traceback (most recent call last):
        File "tools/testing/kunit/kunit.py", line 23, in <module>
          import kunit_json
      ModuleNotFoundError: No module named 'kunit_json'
    
    Add the script's directory to sys.path before the local imports in the two
    executable scripts that have them, kunit.py and kunit_tool_test.py,
    following commit e6c430f5a2cc ("selftests/damon: add script dir to
    sys.path for PYTHONSAFEPATH compatibility") which fixed the same breakage
    for the DAMON selftests.
    
    Fixes: 9f055df11343 ("rh10: import RHEL10 kernel-6.12.0-211.16.1.el10")
    Feature: fix KUnit tests
    https://virtuozzo.atlassian.net/browse/VSTOR-134732
    Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 tools/testing/kunit/kunit.py           | 1 +
 tools/testing/kunit/kunit_tool_test.py | 2 ++
 2 files changed, 3 insertions(+)

diff --git a/tools/testing/kunit/kunit.py b/tools/testing/kunit/kunit.py
index 13ccb99937764..b4bc6c7427182 100755
--- a/tools/testing/kunit/kunit.py
+++ b/tools/testing/kunit/kunit.py
@@ -20,6 +20,7 @@ from dataclasses import dataclass
 from enum import Enum, auto
 from typing import Iterable, List, Optional, Sequence, Tuple
 
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
 import kunit_json
 import kunit_kernel
 import kunit_parser
diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py
index 3121e133e9492..265a8999aada7 100755
--- a/tools/testing/kunit/kunit_tool_test.py
+++ b/tools/testing/kunit/kunit_tool_test.py
@@ -16,8 +16,10 @@ import json
 import os
 import signal
 import subprocess
+import sys
 from typing import Iterable
 
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
 import kunit_config
 import kunit_parser
 import kunit_kernel

^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH vz10 v2] selftests/uevent: do not fail on a netlink receive buffer overrun
  @ 2026-08-24 13:09  3% ` Konstantin Khorenko
  2026-08-26 16:49  0%   ` Konstantin Khorenko
                     ` (2 more replies)
  0 siblings, 3 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-24 13:09 UTC (permalink / raw)


SO_RCVBUF is set to __UEVENT_BUFFER_SIZE, 4KB, which a busy machine
overruns while the test is listening:

  No buffer space available - Failed to receive uevent

Two things are wrong here.

The socket queue is sized after a single message, while do_test()
deliberately triggers ten uevents "to account for the case where the
kernel might drop some", so the queue has to hold more than one.

Give it its own size and leave the message buffer alone: the kernel caps
a single uevent at UEVENT_BUFFER_SIZE, 2048 bytes, so 4KB per message is
already generous.

The receive loop then treats every error as fatal, ENOBUFS included,
which defeats those ten uevents. Netlink clears the error after
reporting it once, so the copies still queued, or still on their way,
are perfectly receivable. Retry instead.

do_test() bounds the listener with a two second sigtimedwait(), so a
retry cannot hang the test.

https://virtuozzo.atlassian.net/browse/VSTOR-139674
Feature: fix selftests
Reported-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
Changes in v2:
- keep __UEVENT_BUFFER_SIZE at 4KB and give SO_RCVBUF its own
  __UEVENT_RCVBUF_SIZE.  v1 raised the shared macro, which sized the
  socket queue correctly but also turned the per message buffer into a
  128KB zero initialized array on the stack, while the kernel caps a
  single uevent at UEVENT_BUFFER_SIZE, 2048 bytes.
- retry recvmsg() on ENOBUFS instead of failing.  v1 only made the
  overrun less likely; the test still died on the first one, even
  though do_test() triggers ten uevents precisely so that drops are
  tolerated.
- subject and commit message updated accordingly.

 tools/testing/selftests/uevent/uevent_filtering.c | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/tools/testing/selftests/uevent/uevent_filtering.c b/tools/testing/selftests/uevent/uevent_filtering.c
index 8062804ff759..735eb8138c44 100644
--- a/tools/testing/selftests/uevent/uevent_filtering.c
+++ b/tools/testing/selftests/uevent/uevent_filtering.c
@@ -23,6 +23,11 @@
 
 #define __DEV_FULL "/sys/devices/virtual/mem/full/uevent"
 #define __UEVENT_BUFFER_SIZE (2048 * 2)
+/*
+ * The socket queue has to hold more than a single message: the test
+ * triggers ten uevents and a busy machine overruns a small buffer.
+ */
+#define __UEVENT_RCVBUF_SIZE (2048 * 64)
 #define __UEVENT_HEADER "add@/devices/virtual/mem/full"
 #define __UEVENT_HEADER_LEN sizeof("add@/devices/virtual/mem/full")
 #define __UEVENT_LISTEN_ALL -1
@@ -78,7 +83,7 @@ static int uevent_listener(unsigned long post_flags, bool expect_uevent,
 {
 	int sk_fd, ret;
 	socklen_t sk_addr_len;
-	int rcv_buf_sz = __UEVENT_BUFFER_SIZE;
+	int rcv_buf_sz = __UEVENT_RCVBUF_SIZE;
 	uint64_t sync_add = 1;
 	struct sockaddr_nl sk_addr = { 0 }, rcv_addr = { 0 };
 	char buf[__UEVENT_BUFFER_SIZE] = { 0 };
@@ -158,6 +163,12 @@ static int uevent_listener(unsigned long post_flags, bool expect_uevent,
 		ssize_t r;
 
 		r = recvmsg(sk_fd, &hdr, 0);
+		/*
+		 * The queue overran.  The kernel clears the error after
+		 * reporting it once and more uevents are on their way.
+		 */
+		if (r < 0 && errno == ENOBUFS)
+			continue;
 		if (r <= 0) {
 			fprintf(stderr, "%s - Failed to receive uevent\n", strerror(errno));
 			ret = -1;
-- 
2.47.1


^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH vz10 1/3] selftests: net: Adapt ethtool mq tests to fix in qdisc graft
  2026-08-21 16:41  4% [Devel] [PATCH vz10 1/3] selftests: net: Adapt ethtool mq tests to fix in qdisc graft Eva Kurchatova
@ 2026-08-24 13:36  0% ` Konstantin Khorenko
  2026-08-24 14:36  3% ` [Devel] [PATCH RHEL10 COMMIT] ms/selftests: " Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-24 13:36 UTC (permalink / raw)


ack

--
Best regards,

Konstantin Khorenko,
Virtuozzo Linux Kernel Team

On 8/21/26 18:41, Eva Kurchatova wrote:
> From: Victor Nogueira <victor@mojatatu.com>
> 
> Because of patch[1] the graft behaviour changed
> 
> So the command:
> 
> tcq replace parent 100:1 handle 204:
> 
> Is no longer valid and will not delete 100:4 added by command:
> 
> tcq replace parent 100:4 handle 204: pfifo_fast
> 
> So to maintain the original behaviour, this patch manually deletes 100:4
> and grafts 100:1
> 
> Note: This change will also work fine without [1]
> 
> [1] https://lore.kernel.org/netdev/20250111151455.75480-1-jhs at mojatatu.com/T/#u
> 
> Signed-off-by: Victor Nogueira <victor@mojatatu.com>
> Reviewed-by: Jamal Hadi Salim <jhs@mojatatu.com>
> Signed-off-by: David S. Miller <davem@davemloft.net>
> (cherry picked from commit 0a5b8fff01bde1b9908f00004c676f2e2459333b)
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-139651
> Feature: fix selftests
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
> ---
>  .../selftests/drivers/net/netdevsim/tc-mq-visibility.sh  | 9 ++++++---
>  1 file changed, 6 insertions(+), 3 deletions(-)
> 
> diff --git a/tools/testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh b/tools/testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh
> index fd13c8cfb7a8..b411fe66510f 100755
> --- a/tools/testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh
> +++ b/tools/testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh
> @@ -58,9 +58,12 @@ for root in mq mqprio; do
>      ethtool -L $NDEV combined 4
>      n_child_assert 4 "One real queue, rest default"
>  
> -    # Graft some
> -    tcq replace parent 100:1 handle 204:
> -    n_child_assert 3 "Grafted"
> +    # Remove real one
> +    tcq del parent 100:4 handle 204:
> +
> +    # Replace default with pfifo
> +    tcq replace parent 100:1 handle 205: pfifo limit 1000
> +    n_child_assert 3 "Deleting real one, replacing default one with pfifo"
>  
>      ethtool -L $NDEV combined 1
>      n_child_assert 1 "Grafted, one"


^ permalink raw reply	[relevance 0%]

* [Devel] [PATCH VZ10 v7 3/9] ve/fs: Rework per-ve mount count
  @ 2026-08-24 13:54  3% ` Vladimir Riabchun
  2026-08-24 13:54  7% ` [Devel] [PATCH VZ10 v7 7/9] ve: Introduce per-VE failcount Vladimir Riabchun
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-08-24 13:54 UTC (permalink / raw)


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     | 73 ++++++++++++++++++++++++++++------------------
 include/linux/ve.h |  2 +-
 kernel/ve/ve.c     | 12 ++++----
 3 files changed, 51 insertions(+), 36 deletions(-)

diff --git a/fs/namespace.c b/fs/namespace.c
index 68e0efb73d7c..e97f48204617 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -317,18 +317,21 @@ 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 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;
 	}
 
@@ -336,6 +339,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;
@@ -370,7 +381,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;
 
@@ -381,6 +393,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;
 }
@@ -750,7 +764,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);
@@ -3205,7 +3221,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.
 			 */
@@ -3344,30 +3360,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)
+static inline int ve_try_reserve_mount(struct ve_struct *ve)
 {
-	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)
-{
-	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;
 }
 
 bool is_sb_ve_accessible(struct ve_struct *ve, struct super_block *sb)
@@ -3389,9 +3405,8 @@ 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) { }
 #endif /* CONFIG_VE */
 
 /*
diff --git a/include/linux/ve.h b/include/linux/ve.h
index 3facbd1759df..cca0a2bc1aac 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -88,7 +88,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 dffb35da22bd..42669a832993 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -81,7 +81,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),
@@ -778,7 +778,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");
@@ -1054,9 +1054,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)
@@ -1616,8 +1616,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


^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH VZ10 v7 7/9] ve: Introduce per-VE failcount
    2026-08-24 13:54  3% ` [Devel] [PATCH VZ10 v7 3/9] ve/fs: Rework per-ve mount count Vladimir Riabchun
@ 2026-08-24 13:54  7% ` Vladimir Riabchun
  2026-08-28 16:52  4%   ` Pavel Tikhomirov
  2026-08-24 13:54  6% ` [Devel] [PATCH VZ10 v7 8/9] selftests/ve: Add more helpers Vladimir Riabchun
  2026-08-24 13:54  5% ` [Devel] [PATCH VZ10 v7 9/9] selftests/ve: Add mount accounting selftest Vladimir Riabchun
  3 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-08-24 13:54 UTC (permalink / raw)


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       |  6 ++++
 kernel/bpf/syscall.c     |  1 +
 kernel/ve/ve.c           | 70 ++++++++++++++++++++++++++++++++++++++++
 net/core/dev.c           |  2 ++
 net/core/neighbour.c     |  1 +
 net/core/net_namespace.c |  4 ++-
 8 files changed, 86 insertions(+), 1 deletion(-)

diff --git a/fs/aio.c b/fs/aio.c
index cb63416af135..3fa07cc626f8 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) {
+		atomic_inc(&ve->aio_failcount);
 		spin_unlock(&ve->aio_nr_lock);
 		err = -EAGAIN;
 		goto err_ctx;
diff --git a/fs/namespace.c b/fs/namespace.c
index e97f48204617..c30bbc370f2b 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -3371,6 +3371,8 @@ static inline int ve_try_reserve_mount(struct ve_struct *ve)
 
 	if (ret)
 		get_ve(ve);
+	else
+		atomic_inc(&ve->mnt_failcount);
 	return ret;
 }
 
diff --git a/include/linux/ve.h b/include/linux/ve.h
index 5687faad46ff..9e73527e970e 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -72,12 +72,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;
@@ -86,6 +89,7 @@ struct ve_struct {
 
 	atomic_t		arp_neigh_nr;
 	atomic_t		nd_neigh_nr;
+	atomic_t		neigh_tbl_failcount;
 	unsigned long		meminfo_val;
 
 	/*
@@ -94,6 +98,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
@@ -121,6 +126,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;
 };
diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
index c94d4240e3d3..9d57e7999ae0 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) {
+			atomic_inc(&load_ve->bpf_prog_failcount);
 			load_ve = NULL;
 			err = -ENOSPC;
 			goto put_token;
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index 0f02835765ff..826f72ad0a22 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -99,10 +99,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	=
@@ -114,8 +117,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,
 	.umh_running_helpers	= ATOMIC_INIT(0),
 	.umh_helpers_waitq	= __WAIT_QUEUE_HEAD_INITIALIZER(ve0.umh_helpers_waitq),
@@ -780,12 +791,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);
@@ -812,7 +826,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
@@ -825,6 +841,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;
@@ -1065,6 +1082,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);
@@ -1602,6 +1666,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..05e0b9b6ba23 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -10997,6 +10997,7 @@ int register_netdevice(struct net_device *dev)
 
 	ret = -ENOMEM;
 	if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
+		atomic_inc(&net->owner_ve->netif_failcount);
 		ve_pr_warn_ratelimited(VE_LOG_BOTH,
 			"CT%s: hits max number of network devices, "
 			"increase ve::netif_max_nr parameter\n",
@@ -12211,6 +12212,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) {
+		atomic_inc(&net->owner_ve->netif_failcount);
 		ve_pr_warn_ratelimited(VE_LOG_BOTH,
 			"CT%s: hits max number of network devices, "
 			"increase ve::netif_max_nr parameter\n",
diff --git a/net/core/neighbour.c b/net/core/neighbour.c
index f90deb17fb25..57a49d9c98a7 100644
--- a/net/core/neighbour.c
+++ b/net/core/neighbour.c
@@ -520,6 +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) {
+			atomic_inc(&ve->neigh_tbl_failcount);
 			net_info_ratelimited("%s: neighbor table overflow!\n",
 					     tbl->id);
 			NEIGH_CACHE_STAT_INC(tbl, table_fulls);
diff --git a/net/core/net_namespace.c b/net/core/net_namespace.c
index b3d54cad984a..9a3376d2682f 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) {
+		atomic_inc(&ve->netns_failcount);
 		return -ENOSPC;
+	}
 	return 0;
 }
 
-- 
2.47.1


^ permalink raw reply	[relevance 7%]

* [Devel] [PATCH VZ10 v7 8/9] selftests/ve: Add more helpers
    2026-08-24 13:54  3% ` [Devel] [PATCH VZ10 v7 3/9] ve/fs: Rework per-ve mount count Vladimir Riabchun
  2026-08-24 13:54  7% ` [Devel] [PATCH VZ10 v7 7/9] ve: Introduce per-VE failcount Vladimir Riabchun
@ 2026-08-24 13:54  6% ` Vladimir Riabchun
  2026-08-24 13:54  5% ` [Devel] [PATCH VZ10 v7 9/9] selftests/ve: Add mount accounting selftest Vladimir Riabchun
  3 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-08-24 13:54 UTC (permalink / raw)


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-135520

Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
 tools/testing/selftests/ve/ve_selftest.h | 81 ++++++++++++++++++++++--
 1 file changed, 75 insertions(+), 6 deletions(-)

diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index 69c0a52dd7ef..48bb7d1871bd 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -43,6 +43,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[20];
+
+	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)
 {
@@ -73,19 +81,31 @@ static inline int read_u64_at(int dirfd, const char *path,
 			      unsigned long long *out)
 {
 	char buf[32] = {0}, *end;
-	int fd, ret;
+	int ret;
 
-	fd = openat(dirfd, path, O_RDONLY);
-	if (fd < 0)
+	ret = read_file_at(dirfd, path, buf, sizeof(buf));
+	if (ret <= 0)
 		return -1;
 
-	ret = read(fd, buf, sizeof(buf) - 1);
-	close(fd);
+	errno = 0;
+	*out = strtoull(buf, &end, 10);
+	if (errno || end == buf)
+		return -1;
+	return 0;
+}
+
+static inline int read_s32_at(int dirfd, const char *path,
+			      int *out)
+{
+	char buf[32] = {0}, *end;
+	int ret;
+
+	ret = read_file_at(dirfd, path, buf, sizeof(buf));
 	if (ret <= 0)
 		return -1;
 
 	errno = 0;
-	*out = strtoull(buf, &end, 10);
+	*out = strtol(buf, &end, 10);
 	if (errno || end == buf)
 		return -1;
 	return 0;
@@ -134,6 +154,55 @@ static inline int enter_cgroup(int cgv2_fd, int ctid)
 	return ret;
 }
 
+/*
+ * Run function in VE cgroup and new namespaces.
+ *
+ * Namespaces are provided via unshare_flags.
+ * CLONE_NEWVE flag is set by this function.
+ * Return values:
+ *  -  0 if function returns zero
+ *  - -1 if function returns negative value
+ *  -  1 if setup fails or function returns positive value
+ */
+static inline int run_in_ve(int cgv2_fd, int ctid, int unshare_flags,
+		int (*fn)(void *), void *arg)
+{
+	int status;
+	pid_t pid;
+
+	unshare_flags |= CLONE_NEWVE;
+	pid = fork();
+	if (pid < 0) {
+		fprintf(stderr, "%s: fork failed\n", __func__);
+		return 1;
+	}
+	if (pid == 0) {
+		int ret;
+
+		if (enter_cgroup(cgv2_fd, ctid) < 0) {
+			fprintf(stderr, "%s: enter_cgroup failed\n", __func__);
+			_exit(255);
+		}
+		if (unshare(unshare_flags) < 0) {
+			fprintf(stderr, "%s: unshare(%d) failed\n",
+				__func__, unshare_flags);
+			_exit(255);
+		}
+		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) == 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.47.1


^ permalink raw reply	[relevance 6%]

* [Devel] [PATCH VZ10 v7 9/9] selftests/ve: Add mount accounting selftest
                     ` (2 preceding siblings ...)
  2026-08-24 13:54  6% ` [Devel] [PATCH VZ10 v7 8/9] selftests/ve: Add more helpers Vladimir Riabchun
@ 2026-08-24 13:54  5% ` Vladimir Riabchun
  3 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-08-24 13:54 UTC (permalink / raw)


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   | 421 ++++++++++++++++++
 3 files changed, 423 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..4426ebc6a1ac
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_mount_accounting_test.c
@@ -0,0 +1,421 @@
+// 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
+
+static int set_pseudosuper(int cgv2_fd, int ctid, int value)
+{
+	char path[64];
+
+	snprintf(path, sizeof(path), "%d/ve.pseudosuper", ctid);
+	return write_u64_at(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 ret;
+}
+
+static int create_mount(int cgv2_fd, int ctid, int id)
+{
+	int ret = run_in_ve(cgv2_fd, ctid, CLONE_NEWVE, _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(int cgv2_fd, int ctid, int id)
+{
+	int ret = run_in_ve(cgv2_fd, ctid, CLONE_NEWVE, _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(int cgv2_fd, int ctid)
+{
+	int avail1, avail2, mnt_id;
+	char path[64];
+
+	mnt_id = get_free_mnt_id();
+
+	snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", ctid);
+	if (mnt_id < 0 ||
+	    read_s32_at(cgv2_fd, path, &avail1) ||
+	    create_mount(cgv2_fd, ctid, mnt_id) ||
+	    read_s32_at(cgv2_fd, path, &avail2) ||
+	    destroy_mount(cgv2_fd, ctid, mnt_id))
+		return -1;
+
+	return avail1 - avail2;
+}
+
+/* Expect mount success and return new avail value */
+static int mount_and_get_avail(struct __test_metadata *_metadata,
+			int cgv2_fd, int ctid, int mnt_id)
+{
+	char path_avail[64];
+	int mnt_avail_nr;
+
+	snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", ctid);
+
+	ASSERT_EQ(create_mount(cgv2_fd, ctid, mnt_id), 0);
+	ASSERT_EQ(read_s32_at(cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	return mnt_avail_nr;
+}
+
+/* Expect mount failure and ensure intact avail number */
+static void assert_mount_fails(struct __test_metadata *_metadata,
+			int cgv2_fd, int ctid, 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", ctid);
+
+	ASSERT_EQ(read_s32_at(cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, avail_count);
+	ASSERT_LT(create_mount(cgv2_fd, ctid, mnt_id), 0);
+	ASSERT_EQ(read_s32_at(cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, avail_count);
+}
+
+FIXTURE(ve_mnt_acc)
+{
+	int cgv2_fd;
+	int ctid;
+};
+
+FIXTURE_SETUP(ve_mnt_acc)
+{
+	unsigned long long 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_u64_at(self->cgv2_fd, path, &initial_mnt_avail_nr), 0);
+	ASSERT_EQ(initial_mnt_avail_nr, VE_MOUNTS_MAX);
+};
+
+FIXTURE_TEARDOWN(ve_mnt_acc)
+{
+	for (int i = 0; i < MAX_MNT_ID; i++)
+		_destroy_mount((void *)&i);
+
+	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->cgv2_fd, self->ctid, 0),
+		  original_mnt_avail);
+
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 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 original_mnt_avail, mnt_avail_nr, mnt_cost;
+	int original_have_mnt;
+	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);
+
+	mnt_cost = get_mount_cost(self->cgv2_fd, self->ctid);
+	ASSERT_GE(mnt_cost, 1);
+
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &original_mnt_avail), 0);
+	original_have_mnt = VE_MOUNTS_MAX - original_mnt_avail;
+
+	/* Step 1: reduce number of available mounts to mnt_cost */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, original_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 2: do one mount, no mounts should be available */
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 0),
+		  0);
+
+	/* Step 3: check that one more mount fails */
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 1, 0);
+
+	/* Step 4: increase mount limit a little bit, mount should still fail */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+				original_have_mnt + 2 * mnt_cost - 1), 0);
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 1, mnt_cost - 1);
+
+	/* Step 5: increase by 1 and win now */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, original_have_mnt + 2 * mnt_cost), 0);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, mnt_cost);
+
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 1),
+		  0);
+
+	/* Step 6: reduce mnt_max_nr so we have more mounts than allowed */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, original_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 7: try to do mount when avail < 0, ensure number is intact */
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, -1 * mnt_cost);
+
+	/* Step 8: remove one mount, check avail value update, mount should fail */
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 0), 0);
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, 0);
+
+	/* Step 9: remove one more mount and check that new mount succeeds */
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 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(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 2),
+		  0);
+
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 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 mount_cost, i, orig_have, orig_mnt_avail;
+
+	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);
+
+	mount_cost = get_mount_cost(self->cgv2_fd, self->ctid);
+	ASSERT_GE(mount_cost, 1);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &orig_mnt_avail), 0);
+	orig_have = VE_MOUNTS_MAX - orig_mnt_avail;
+
+	if (mount_cost == 1)
+		SKIP(return, "mount cost is 1, no partial mounts possible");
+
+	for (i = 0; i < mount_cost; i++) {
+		ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, orig_have + i), 0);
+		assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 0, i);
+	}
+}
+
+/* Test that pseudosuper allows negative avail with correct accounting. */
+TEST_F(ve_mnt_acc, pseudosuper_allows_overuse)
+{
+	int orig_mnt_avail, orig_have;
+	int mount_cost;
+	char path_avail[64], path_max_nr[64];
+
+	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);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &orig_mnt_avail), 0);
+	orig_have = VE_MOUNTS_MAX - orig_mnt_avail;
+
+	mount_cost = get_mount_cost(self->cgv2_fd, self->ctid);
+	ASSERT_GE(mount_cost, 1);
+
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, orig_have + mount_cost), 0);
+	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 1), 0);
+
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 0),
+		  0);
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 1),
+		  -1 * mount_cost);
+
+	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 0), 0);
+
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, -1 * mount_cost);
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 0), 0);
+	assert_mount_fails(_metadata, self->cgv2_fd, self->ctid, 2, 0);
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 1), 0);
+	ASSERT_EQ(get_mount_cost(self->cgv2_fd, self->ctid), mount_cost);
+}
+
+/* Test that pseudosuper doesn't disable accounting. */
+TEST_F(ve_mnt_acc, pseudosuper_continues_accounting)
+{
+	int orig_mnt_avail, mount_cost, 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);
+	mount_cost = get_mount_cost(self->cgv2_fd, self->ctid);
+	ASSERT_GE(mount_cost, 1);
+
+	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 0), 0);
+
+	/* mnt 0 - mounted without pseudosuper, umounted with it. */
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 0),
+		  orig_mnt_avail - mount_cost);
+	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 1), 0);
+
+	/* Cost is the same when mount/umount happen under pseudosuper. */
+	ASSERT_EQ(get_mount_cost(self->cgv2_fd, self->ctid), mount_cost);
+
+	/* mnt 1 - mounted with pseudosuper, umounted without it. */
+	ASSERT_EQ(mount_and_get_avail(_metadata, self->cgv2_fd, self->ctid, 1),
+		  orig_mnt_avail - 2 * mount_cost);
+
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 0), 0);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, orig_mnt_avail - mount_cost);
+
+	ASSERT_EQ(set_pseudosuper(self->cgv2_fd, self->ctid, 0), 0);
+
+	ASSERT_EQ(destroy_mount(self->cgv2_fd, self->ctid, 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->cgv2_fd, self->ctid, 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->cgv2_fd, self->ctid, 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->cgv2_fd, self->ctid, 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->cgv2_fd, self->ctid, 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);
+}
+
+TEST_HARNESS_MAIN
-- 
2.47.1


^ permalink raw reply	[relevance 5%]

* [Devel] [PATCH RHEL10 COMMIT] ms/selftests: net: Adapt ethtool mq tests to fix in qdisc graft
@ 2026-08-24 14:36  4% Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-24 14:36 UTC (permalink / raw)


The commit is pushed to "branch-rh10-6.12.0-211.39.1.16.x.vz10-ovz" and will appear at git at bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.39.1.16.8.vz10
------>
commit b7c858a8bd7145f399251227af90c663ac1154e3
Author: Victor Nogueira <victor@mojatatu.com>
Date:   Sat Jan 11 18:15:15 2025 -0300

    ms/selftests: net: Adapt ethtool mq tests to fix in qdisc graft
    
    Because of patch[1] the graft behaviour changed
    
    So the command:
    
    tcq replace parent 100:1 handle 204:
    
    Is no longer valid and will not delete 100:4 added by command:
    
    tcq replace parent 100:4 handle 204: pfifo_fast
    
    So to maintain the original behaviour, this patch manually deletes 100:4
    and grafts 100:1
    
    Note: This change will also work fine without [1]
    
    [1] https://lore.kernel.org/netdev/20250111151455.75480-1-jhs at mojatatu.com/T/#u
    
    Signed-off-by: Victor Nogueira <victor@mojatatu.com>
    Reviewed-by: Jamal Hadi Salim <jhs@mojatatu.com>
    Signed-off-by: David S. Miller <davem@davemloft.net>
    (cherry picked from commit 0a5b8fff01bde1b9908f00004c676f2e2459333b)
    
    https://virtuozzo.atlassian.net/browse/VSTOR-139651
    Feature: fix selftests
    Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 .../testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh  | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/tools/testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh b/tools/testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh
index fd13c8cfb7a8..b411fe66510f 100755
--- a/tools/testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh
+++ b/tools/testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh
@@ -58,9 +58,12 @@ for root in mq mqprio; do
     ethtool -L $NDEV combined 4
     n_child_assert 4 "One real queue, rest default"
 
-    # Graft some
-    tcq replace parent 100:1 handle 204:
-    n_child_assert 3 "Grafted"
+    # Remove real one
+    tcq del parent 100:4 handle 204:
+
+    # Replace default with pfifo
+    tcq replace parent 100:1 handle 205: pfifo limit 1000
+    n_child_assert 3 "Deleting real one, replacing default one with pfifo"
 
     ethtool -L $NDEV combined 1
     n_child_assert 1 "Grafted, one"

^ permalink raw reply	[relevance 4%]

* [Devel] [PATCH RHEL10 COMMIT] ms/selftests: net: Adapt ethtool mq tests to fix in qdisc graft
  2026-08-21 16:41  4% [Devel] [PATCH vz10 1/3] selftests: net: Adapt ethtool mq tests to fix in qdisc graft Eva Kurchatova
  2026-08-24 13:36  0% ` Konstantin Khorenko
@ 2026-08-24 14:36  3% ` Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-24 14:36 UTC (permalink / raw)


The commit is pushed to "branch-rh10-6.12.0-211.39.1.16.x.vz10-ovz" and will appear at git at bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.39.1.16.8.vz10
------>
commit edf5885dd6749ceb3a09c536a5d1ff81cc45cf66
Author: Victor Nogueira <victor@mojatatu.com>
Date:   Sat Jan 11 18:15:15 2025 -0300

    ms/selftests: net: Adapt ethtool mq tests to fix in qdisc graft
    
    Because of patch[1] the graft behaviour changed
    
    So the command:
    
    tcq replace parent 100:1 handle 204:
    
    Is no longer valid and will not delete 100:4 added by command:
    
    tcq replace parent 100:4 handle 204: pfifo_fast
    
    So to maintain the original behaviour, this patch manually deletes 100:4
    and grafts 100:1
    
    Note: This change will also work fine without [1]
    
    [1] https://lore.kernel.org/netdev/20250111151455.75480-1-jhs at mojatatu.com/T/#u
    
    Signed-off-by: Victor Nogueira <victor@mojatatu.com>
    Reviewed-by: Jamal Hadi Salim <jhs@mojatatu.com>
    Signed-off-by: David S. Miller <davem@davemloft.net>
    (cherry picked from commit 0a5b8fff01bde1b9908f00004c676f2e2459333b)
    
    https://virtuozzo.atlassian.net/browse/VSTOR-139651
    Feature: fix selftests
    Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
    Reviewed-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 .../testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh  | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/tools/testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh b/tools/testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh
index fd13c8cfb7a8..b411fe66510f 100755
--- a/tools/testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh
+++ b/tools/testing/selftests/drivers/net/netdevsim/tc-mq-visibility.sh
@@ -58,9 +58,12 @@ for root in mq mqprio; do
     ethtool -L $NDEV combined 4
     n_child_assert 4 "One real queue, rest default"
 
-    # Graft some
-    tcq replace parent 100:1 handle 204:
-    n_child_assert 3 "Grafted"
+    # Remove real one
+    tcq del parent 100:4 handle 204:
+
+    # Replace default with pfifo
+    tcq replace parent 100:1 handle 205: pfifo limit 1000
+    n_child_assert 3 "Deleting real one, replacing default one with pfifo"
 
     ethtool -L $NDEV combined 1
     n_child_assert 1 "Grafted, one"

^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH vz10 v2] selftests: ve_printk: match the conntrack overflow message
  @ 2026-08-24 14:53  3% ` Konstantin Khorenko
  2026-08-24 14:54  3%   ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
  0 siblings, 1 reply; 200+ results
From: Konstantin Khorenko @ 2026-08-24 14:53 UTC (permalink / raw)


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

The kernel prints

  VE%s: nf_conntrack table full in netns %u, dropping packet

while the test looks for "nf_conntrack table full, dropping packet".
The "in netns %u" part sits between the two halves the test expects to
be adjacent, so strstr() never matched: ve_log_both counted zero
messages and failed even when the container produced all ten of them.

Match the part of the message which carries no netns number, and drop
the stale quote from the comment above the test.

Fixes: d5ec8836f50d ("tests: add ve_printk selftest")
https://virtuozzo.atlassian.net/browse/VSTOR-139673
Feature: fix vz selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
Changes in v2:
- fix the stale quote in the comment above ve_log_both as well.  It
  still spelled out the message the test used to look for, which is
  exactly what would mislead the next reader.
- add a Fixes: tag.  The kernel message has carried "in netns %u"
  since 2015, so the test has been looking for a string that never
  existed rather than falling behind a later kernel change.
- commit message: quote the kernel format string instead of a made up
  log line with a VE0 prefix.  Inside the test the message is emitted
  for the container's netns, so the prefix is the container, not VE0.

 tools/testing/selftests/ve_printk/ve_printk_test.c | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/tools/testing/selftests/ve_printk/ve_printk_test.c b/tools/testing/selftests/ve_printk/ve_printk_test.c
index 2df672d75e00..020df691e5d5 100644
--- a/tools/testing/selftests/ve_printk/ve_printk_test.c
+++ b/tools/testing/selftests/ve_printk/ve_printk_test.c
@@ -286,7 +286,7 @@ int ve_printk_test_logboth(void)
 	ret = TEST_RATELIMIT_BURST;
 	/* verify that only 10 records were added */
 	while (fgets(buf, sizeof(buf), pdmesg)) {
-		if (strstr(buf, "nf_conntrack table full, dropping packet")) {
+		if (strstr(buf, "nf_conntrack table full")) {
 			ret--;
 		}
 	}
@@ -614,10 +614,10 @@ TEST_F(ve_printk, ve0_log)
  * Test verifies net_veboth_ratelimited function which logs messages simultaneously
  * to both the container (VE_LOG) and VE0 (VE0_LOG), but with ratelimit throttling.
  * Inside the container, a small conntrack table (size 2) is configured, then many ping
- * packets are sent, causing table overflow and generation of "nf_conntrack table full,
- * dropping packet" messages. The test verifies that exactly TEST_RATELIMIT_BURST (10)
- * messages appeared on the host - ratelimit should limit the number of messages even
- * if more were generated.
+ * packets are sent, causing table overflow and generation of "nf_conntrack table
+ * full in netns N, dropping packet" messages. The test verifies that exactly
+ * TEST_RATELIMIT_BURST (10) messages appeared on the host - ratelimit should
+ * limit the number of messages even if more were generated.
  */
 TEST_F(ve_printk, ve_log_both)
 {
@@ -635,7 +635,7 @@ TEST_F(ve_printk, ve_log_both)
 	ASSERT_EQ(ret, 0);
 
 	while (fgets(buf, sizeof(buf), fdmesg)) {
-		if (strstr(buf, "nf_conntrack table full, dropping packet")) {
+		if (strstr(buf, "nf_conntrack table full")) {
 			ret++;
 		}
 	}
-- 
2.47.1


^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH RHEL10 COMMIT] selftests: ve_printk: match the conntrack overflow message
  2026-08-24 14:53  3% ` [Devel] [PATCH vz10 v2] " Konstantin Khorenko
@ 2026-08-24 14:54  3%   ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-24 14:54 UTC (permalink / raw)


The commit is pushed to "branch-rh10-6.12.0-211.39.1.16.x.vz10-ovz" and will appear at git at bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.39.1.16.8.vz10
------>
commit b8602e1ad08847d26d25dbf1426cf1247ef24c26
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
Date:   Fri Aug 21 18:32:21 2026 +0300

    selftests: ve_printk: match the conntrack overflow message
    
    The kernel prints
    
      VE%s: nf_conntrack table full in netns %u, dropping packet
    
    while the test looks for "nf_conntrack table full, dropping packet".
    The "in netns %u" part sits between the two halves the test expects to
    be adjacent, so strstr() never matched: ve_log_both counted zero
    messages and failed even when the container produced all ten of them.
    
    Match the part of the message which carries no netns number, and drop
    the stale quote from the comment above the test.
    
    Fixes: d5ec8836f50d ("tests: add ve_printk selftest")
    https://virtuozzo.atlassian.net/browse/VSTOR-139673
    Feature: fix vz selftests
    Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
    Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 tools/testing/selftests/ve_printk/ve_printk_test.c | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/tools/testing/selftests/ve_printk/ve_printk_test.c b/tools/testing/selftests/ve_printk/ve_printk_test.c
index 2df672d75e00..020df691e5d5 100644
--- a/tools/testing/selftests/ve_printk/ve_printk_test.c
+++ b/tools/testing/selftests/ve_printk/ve_printk_test.c
@@ -286,7 +286,7 @@ int ve_printk_test_logboth(void)
 	ret = TEST_RATELIMIT_BURST;
 	/* verify that only 10 records were added */
 	while (fgets(buf, sizeof(buf), pdmesg)) {
-		if (strstr(buf, "nf_conntrack table full, dropping packet")) {
+		if (strstr(buf, "nf_conntrack table full")) {
 			ret--;
 		}
 	}
@@ -614,10 +614,10 @@ TEST_F(ve_printk, ve0_log)
  * Test verifies net_veboth_ratelimited function which logs messages simultaneously
  * to both the container (VE_LOG) and VE0 (VE0_LOG), but with ratelimit throttling.
  * Inside the container, a small conntrack table (size 2) is configured, then many ping
- * packets are sent, causing table overflow and generation of "nf_conntrack table full,
- * dropping packet" messages. The test verifies that exactly TEST_RATELIMIT_BURST (10)
- * messages appeared on the host - ratelimit should limit the number of messages even
- * if more were generated.
+ * packets are sent, causing table overflow and generation of "nf_conntrack table
+ * full in netns N, dropping packet" messages. The test verifies that exactly
+ * TEST_RATELIMIT_BURST (10) messages appeared on the host - ratelimit should
+ * limit the number of messages even if more were generated.
  */
 TEST_F(ve_printk, ve_log_both)
 {
@@ -635,7 +635,7 @@ TEST_F(ve_printk, ve_log_both)
 	ASSERT_EQ(ret, 0);
 
 	while (fgets(buf, sizeof(buf), fdmesg)) {
-		if (strstr(buf, "nf_conntrack table full, dropping packet")) {
+		if (strstr(buf, "nf_conntrack table full")) {
 			ret++;
 		}
 	}

^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH VZ10 v2 5/5] drivers/vhost/blk: rework queue/backend setup
  @ 2026-08-24 15:59  7% ` Andrey Zhadchenko
  0 siblings, 0 replies; 200+ results
From: Andrey Zhadchenko @ 2026-08-24 15:59 UTC (permalink / raw)


vhost_blk_setup() is pretty bad: silently refusing changed vq->num
if requests are already allocated, fetching user input second time
(double-fetch vulnerability).
To handle this, tie request allocation to backend existence. After
all, if there is no backend, there is no point in having requests.
Also expand it to get rid of boilerplate drop_backend, flush,
fput sequence in a few places.

https://virtuozzo.atlassian.net/browse/VSTOR-138640
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
 drivers/vhost/blk.c | 126 +++++++++++++++++++-------------------------
 1 file changed, 54 insertions(+), 72 deletions(-)

diff --git a/drivers/vhost/blk.c b/drivers/vhost/blk.c
index 6a483e527990e..456ccfbe369f0 100644
--- a/drivers/vhost/blk.c
+++ b/drivers/vhost/blk.c
@@ -658,11 +658,14 @@ static void vhost_blk_flush(struct vhost_blk *blk)
 	spin_unlock(&blk->flush_lock);
 }
 
-static inline void vhost_blk_drop_backends(struct vhost_blk *blk)
+static void vhost_blk_drop_backend(struct vhost_blk *blk)
 {
 	struct vhost_virtqueue *vq;
 	int i;
 
+	if (!blk->backend)
+		return;
+
 	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
 		vq = &blk->vqs[i].vq;
 
@@ -670,6 +673,44 @@ static inline void vhost_blk_drop_backends(struct vhost_blk *blk)
 		vhost_vq_set_backend(vq, NULL);
 		mutex_unlock(&vq->mutex);
 	}
+
+	vhost_blk_flush(blk);
+
+	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
+		kvfree(blk->vqs[i].req);
+		blk->vqs[i].req = NULL;
+	}
+
+	fput(blk->backend);
+	blk->backend = NULL;
+}
+
+static int vhost_blk_setup_vqs(struct vhost_blk *blk)
+{
+	struct vhost_virtqueue *vq;
+	int i;
+
+	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
+		vq = &blk->vqs[i].vq;
+
+		if (!vhost_vq_is_setup(vq))
+			continue;
+
+		blk->vqs[i].req = kvmalloc_array(vq->num, sizeof(struct vhost_blk_req),
+						 GFP_KERNEL);
+		if (!blk->vqs[i].req)
+			return -ENOMEM;
+
+		mutex_lock(&vq->mutex);
+		vhost_vq_set_backend(vq, blk->backend);
+		if (vhost_vq_init_access(vq)) {
+			mutex_unlock(&vq->mutex);
+			return -EFAULT;
+		}
+		mutex_unlock(&vq->mutex);
+	}
+
+	return 0;
 }
 
 static int vhost_blk_open(struct inode *inode, struct file *file)
@@ -722,16 +763,10 @@ static int vhost_blk_open(struct inode *inode, struct file *file)
 static int vhost_blk_release(struct inode *inode, struct file *f)
 {
 	struct vhost_blk *blk = f->private_data;
-	int i;
 
-	vhost_blk_drop_backends(blk);
-	vhost_blk_flush(blk);
+	vhost_blk_drop_backend(blk);
 	vhost_dev_stop(&blk->dev);
-	if (blk->backend)
-		fput(blk->backend);
 	vhost_dev_cleanup(&blk->dev);
-	for (i = 0; i < VHOST_BLK_VQ_MAX; i++)
-		kvfree(blk->vqs[i].req);
 	kfree(blk->dev.vqs);
 	kvfree(blk);
 
@@ -765,32 +800,19 @@ static int vhost_blk_set_features(struct vhost_blk *blk, u64 features)
 
 static long vhost_blk_set_backend(struct vhost_blk *blk, int fd)
 {
-	struct vhost_virtqueue *vq;
 	struct file *file;
 	struct inode *inode;
-	int ret, i;
+	int ret;
 
 	mutex_lock(&blk->dev.mutex);
 	ret = vhost_dev_check_owner(&blk->dev);
 	if (ret)
 		goto out_dev;
 
-	/*
-	 * fd < 0 means "stop the device".  Detach the backend from every vq so
-	 * vhost_blk_handle_guest_kick() stops fetching descriptors, drain the
-	 * in-flight requests, and release the backing file.
-	 */
+	/* fd < 0 means "stop the device" */
 	if (fd < 0) {
-		if (!blk->backend) {
-			ret = 0;		/* already stopped */
-			goto out_dev;
-		}
-		vhost_blk_drop_backends(blk);
-		vhost_blk_flush(blk);
-		fput(blk->backend);
-		blk->backend = NULL;
 		ret = 0;
-		goto out_dev;
+		goto out_drop;
 	}
 
 	if (blk->backend) {
@@ -807,31 +829,20 @@ static long vhost_blk_set_backend(struct vhost_blk *blk, int fd)
 	inode = file->f_mapping->host;
 	if (!S_ISBLK(inode->i_mode)) {
 		ret = -EFAULT;
-		goto out_file;
-	}
-
-	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
-		vq = &blk->vqs[i].vq;
-		if (!vhost_vq_access_ok(vq)) {
-			ret = -EFAULT;
-			goto out_drop;
-		}
-
-		mutex_lock(&vq->mutex);
-		vhost_vq_set_backend(vq, file);
-		ret = vhost_vq_init_access(vq);
-		mutex_unlock(&vq->mutex);
+		fput(file);
+		goto out_dev;
 	}
 
 	blk->backend = file;
+	ret = vhost_blk_setup_vqs(blk);
+	if (ret)
+		goto out_drop;
 
 	mutex_unlock(&blk->dev.mutex);
 	return 0;
 
 out_drop:
-	vhost_blk_drop_backends(blk);
-out_file:
-	fput(file);
+	vhost_blk_drop_backend(blk);
 out_dev:
 	mutex_unlock(&blk->dev.mutex);
 	return ret;
@@ -840,7 +851,7 @@ static long vhost_blk_set_backend(struct vhost_blk *blk, int fd)
 static long vhost_blk_reset_owner(struct vhost_blk *blk)
 {
 	struct vhost_iotlb *umem;
-	int err, i;
+	int err;
 
 	mutex_lock(&blk->dev.mutex);
 	err = vhost_dev_check_owner(&blk->dev);
@@ -851,42 +862,15 @@ static long vhost_blk_reset_owner(struct vhost_blk *blk)
 		err = -ENOMEM;
 		goto done;
 	}
-	vhost_blk_drop_backends(blk);
-	if (blk->backend) {
-		fput(blk->backend);
-		blk->backend = NULL;
-	}
-	vhost_blk_flush(blk);
+	vhost_blk_drop_backend(blk);
 	vhost_dev_stop(&blk->dev);
 	vhost_dev_reset_owner(&blk->dev, umem);
 
-	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
-		kvfree(blk->vqs[i].req);
-		blk->vqs[i].req = NULL;
-	}
-
 done:
 	mutex_unlock(&blk->dev.mutex);
 	return err;
 }
 
-static int vhost_blk_setup(struct vhost_blk *blk, void __user *argp)
-{
-	struct vhost_vring_state s;
-
-	if (copy_from_user(&s, argp, sizeof(s)))
-		return -EFAULT;
-
-	if (blk->vqs[s.index].req)
-		return 0;
-
-	blk->vqs[s.index].req = kvmalloc(sizeof(struct vhost_blk_req) * s.num, GFP_KERNEL);
-	if (!blk->vqs[s.index].req)
-		return -ENOMEM;
-
-	return 0;
-}
-
 static long vhost_blk_ioctl(struct file *f, unsigned int ioctl,
 			    unsigned long arg)
 {
@@ -924,8 +908,6 @@ static long vhost_blk_ioctl(struct file *f, unsigned int ioctl,
 		ret = vhost_dev_ioctl(&blk->dev, ioctl, argp);
 		if (ret == -ENOIOCTLCMD)
 			ret = vhost_vring_ioctl(&blk->dev, ioctl, argp);
-		if (!ret && ioctl == VHOST_SET_VRING_NUM)
-			ret = vhost_blk_setup(blk, argp);
 		vhost_blk_flush(blk);
 		mutex_unlock(&blk->dev.mutex);
 		return ret;
-- 
2.43.5


^ permalink raw reply	[relevance 7%]

* [Devel] [PATCH VZ10 v3 5/5] drivers/vhost/blk: rework queue/backend setup
  @ 2026-08-25 12:51  7% ` Andrey Zhadchenko
  2026-08-25 13:57  7%   ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
  0 siblings, 1 reply; 200+ results
From: Andrey Zhadchenko @ 2026-08-25 12:51 UTC (permalink / raw)


vhost_blk_setup() is pretty bad: silently refusing changed vq->num
if requests are already allocated, fetching user input second time
(double-fetch vulnerability).
To handle this, tie request allocation to backend existence. After
all, if there is no backend, there is no point in having requests.
Also expand it to get rid of boilerplate drop_backend, flush,
fput sequence in a few places.

https://virtuozzo.atlassian.net/browse/VSTOR-138640
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
 drivers/vhost/blk.c | 126 +++++++++++++++++++-------------------------
 1 file changed, 54 insertions(+), 72 deletions(-)

diff --git a/drivers/vhost/blk.c b/drivers/vhost/blk.c
index b2cf111bbb455..8e3025e934445 100644
--- a/drivers/vhost/blk.c
+++ b/drivers/vhost/blk.c
@@ -658,11 +658,14 @@ static void vhost_blk_flush(struct vhost_blk *blk)
 	spin_unlock(&blk->flush_lock);
 }
 
-static inline void vhost_blk_drop_backends(struct vhost_blk *blk)
+static void vhost_blk_drop_backend(struct vhost_blk *blk)
 {
 	struct vhost_virtqueue *vq;
 	int i;
 
+	if (!blk->backend)
+		return;
+
 	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
 		vq = &blk->vqs[i].vq;
 
@@ -670,6 +673,44 @@ static inline void vhost_blk_drop_backends(struct vhost_blk *blk)
 		vhost_vq_set_backend(vq, NULL);
 		mutex_unlock(&vq->mutex);
 	}
+
+	vhost_blk_flush(blk);
+
+	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
+		kvfree(blk->vqs[i].req);
+		blk->vqs[i].req = NULL;
+	}
+
+	fput(blk->backend);
+	blk->backend = NULL;
+}
+
+static int vhost_blk_setup_vqs(struct vhost_blk *blk)
+{
+	struct vhost_virtqueue *vq;
+	int i;
+
+	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
+		vq = &blk->vqs[i].vq;
+
+		if (!vhost_vq_is_setup(vq))
+			continue;
+
+		blk->vqs[i].req = kvmalloc_array(vq->num, sizeof(struct vhost_blk_req),
+						 GFP_KERNEL);
+		if (!blk->vqs[i].req)
+			return -ENOMEM;
+
+		mutex_lock(&vq->mutex);
+		vhost_vq_set_backend(vq, blk->backend);
+		if (vhost_vq_init_access(vq)) {
+			mutex_unlock(&vq->mutex);
+			return -EFAULT;
+		}
+		mutex_unlock(&vq->mutex);
+	}
+
+	return 0;
 }
 
 static int vhost_blk_open(struct inode *inode, struct file *file)
@@ -722,16 +763,10 @@ static int vhost_blk_open(struct inode *inode, struct file *file)
 static int vhost_blk_release(struct inode *inode, struct file *f)
 {
 	struct vhost_blk *blk = f->private_data;
-	int i;
 
-	vhost_blk_drop_backends(blk);
-	vhost_blk_flush(blk);
+	vhost_blk_drop_backend(blk);
 	vhost_dev_stop(&blk->dev);
-	if (blk->backend)
-		fput(blk->backend);
 	vhost_dev_cleanup(&blk->dev);
-	for (i = 0; i < VHOST_BLK_VQ_MAX; i++)
-		kvfree(blk->vqs[i].req);
 	kfree(blk->dev.vqs);
 	kvfree(blk);
 
@@ -765,32 +800,19 @@ static int vhost_blk_set_features(struct vhost_blk *blk, u64 features)
 
 static long vhost_blk_set_backend(struct vhost_blk *blk, int fd)
 {
-	struct vhost_virtqueue *vq;
 	struct file *file;
 	struct inode *inode;
-	int ret, i;
+	int ret;
 
 	mutex_lock(&blk->dev.mutex);
 	ret = vhost_dev_check_owner(&blk->dev);
 	if (ret)
 		goto out_dev;
 
-	/*
-	 * fd < 0 means "stop the device".  Detach the backend from every vq so
-	 * vhost_blk_handle_guest_kick() stops fetching descriptors, drain the
-	 * in-flight requests, and release the backing file.
-	 */
+	/* fd < 0 means "stop the device" */
 	if (fd < 0) {
-		if (!blk->backend) {
-			ret = 0;		/* already stopped */
-			goto out_dev;
-		}
-		vhost_blk_drop_backends(blk);
-		vhost_blk_flush(blk);
-		fput(blk->backend);
-		blk->backend = NULL;
 		ret = 0;
-		goto out_dev;
+		goto out_drop;
 	}
 
 	if (blk->backend) {
@@ -807,31 +829,20 @@ static long vhost_blk_set_backend(struct vhost_blk *blk, int fd)
 	inode = file->f_mapping->host;
 	if (!S_ISBLK(inode->i_mode)) {
 		ret = -EFAULT;
-		goto out_file;
-	}
-
-	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
-		vq = &blk->vqs[i].vq;
-		if (!vhost_vq_access_ok(vq)) {
-			ret = -EFAULT;
-			goto out_drop;
-		}
-
-		mutex_lock(&vq->mutex);
-		vhost_vq_set_backend(vq, file);
-		ret = vhost_vq_init_access(vq);
-		mutex_unlock(&vq->mutex);
+		fput(file);
+		goto out_dev;
 	}
 
 	blk->backend = file;
+	ret = vhost_blk_setup_vqs(blk);
+	if (ret)
+		goto out_drop;
 
 	mutex_unlock(&blk->dev.mutex);
 	return 0;
 
 out_drop:
-	vhost_blk_drop_backends(blk);
-out_file:
-	fput(file);
+	vhost_blk_drop_backend(blk);
 out_dev:
 	mutex_unlock(&blk->dev.mutex);
 	return ret;
@@ -840,7 +851,7 @@ static long vhost_blk_set_backend(struct vhost_blk *blk, int fd)
 static long vhost_blk_reset_owner(struct vhost_blk *blk)
 {
 	struct vhost_iotlb *umem;
-	int err, i;
+	int err;
 
 	mutex_lock(&blk->dev.mutex);
 	err = vhost_dev_check_owner(&blk->dev);
@@ -851,42 +862,15 @@ static long vhost_blk_reset_owner(struct vhost_blk *blk)
 		err = -ENOMEM;
 		goto done;
 	}
-	vhost_blk_drop_backends(blk);
-	if (blk->backend) {
-		fput(blk->backend);
-		blk->backend = NULL;
-	}
-	vhost_blk_flush(blk);
+	vhost_blk_drop_backend(blk);
 	vhost_dev_stop(&blk->dev);
 	vhost_dev_reset_owner(&blk->dev, umem);
 
-	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
-		kvfree(blk->vqs[i].req);
-		blk->vqs[i].req = NULL;
-	}
-
 done:
 	mutex_unlock(&blk->dev.mutex);
 	return err;
 }
 
-static int vhost_blk_setup(struct vhost_blk *blk, void __user *argp)
-{
-	struct vhost_vring_state s;
-
-	if (copy_from_user(&s, argp, sizeof(s)))
-		return -EFAULT;
-
-	if (blk->vqs[s.index].req)
-		return 0;
-
-	blk->vqs[s.index].req = kvmalloc(sizeof(struct vhost_blk_req) * s.num, GFP_KERNEL);
-	if (!blk->vqs[s.index].req)
-		return -ENOMEM;
-
-	return 0;
-}
-
 static long vhost_blk_ioctl(struct file *f, unsigned int ioctl,
 			    unsigned long arg)
 {
@@ -924,8 +908,6 @@ static long vhost_blk_ioctl(struct file *f, unsigned int ioctl,
 		ret = vhost_dev_ioctl(&blk->dev, ioctl, argp);
 		if (ret == -ENOIOCTLCMD)
 			ret = vhost_vring_ioctl(&blk->dev, ioctl, argp);
-		if (!ret && ioctl == VHOST_SET_VRING_NUM)
-			ret = vhost_blk_setup(blk, argp);
 		vhost_blk_flush(blk);
 		mutex_unlock(&blk->dev.mutex);
 		return ret;
-- 
2.43.5


^ permalink raw reply	[relevance 7%]

* [Devel] [PATCH vz10 2/2] selftests/ve: check that hiding an entry does not unmount it
  @ 2026-08-25 13:29  5% ` Mirian Shilakadze
  2026-08-25 15:40  0%   ` Pavel Tikhomirov
  0 siblings, 1 reply; 200+ results
From: Mirian Shilakadze @ 2026-08-25 13:29 UTC (permalink / raw)


kernfs_dop_revalidate() answered the per VE visibility check with the same
"return 0" the staleness checks use, and the VFS reads 0 as a global fact:
d_invalidate() hands every mountpoint under that dentry to
__detach_mounts(), whose mountpoint hash is not scoped to a mount
namespace.  A single lookup from inside a Container unmounted the host's
bpffs, and libvzctl needs bpffs for the cgroup v2 device controller, so
the whole node stopped being manageable.

Mount a tmpfs on the entry the variant already keeps host only, look it up
from inside a VE, and require both that the VE is told ENOENT and that the
mount is still there afterwards.  The mount is made in the test's own
mount namespace so the machine running the test cannot lose a mount it
needs, while the dentry the mount hangs on is still the shared one the bug
worked through.

Fails without the preceding fix, on both the sysfs and the proc variant.

Feature: kernfs: per-CT entries visibility and permissions configuration
https://virtuozzo.atlassian.net/browse/VSTOR-142552
Signed-off-by: Mirian Shilakadze <mirian.shilakadze@virtuozzo.com>
---
 tools/testing/selftests/ve/ve_perms_test.c | 52 ++++++++++++++++++++++
 tools/testing/selftests/ve/ve_selftest.h   | 27 ++++++++++-
 2 files changed, 77 insertions(+), 2 deletions(-)

diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
index 4522950c17f2..25ffbd42c380 100644
--- a/tools/testing/selftests/ve/ve_perms_test.c
+++ b/tools/testing/selftests/ve/ve_perms_test.c
@@ -24,6 +24,7 @@
 #include <unistd.h>
 #include <fcntl.h>
 #include <limits.h>
+#include <sys/mount.h>
 #include <sys/wait.h>
 #include <errno.h>
 
@@ -412,4 +413,55 @@ TEST_F(ve_perms, enforce_denies)
 			      absent, O_RDONLY), EACCES);
 }
 
+/*
+ * Looking up an entry that a VE cannot see must not disturb a mount that
+ * sits on it.
+ *
+ * The lookup used to answer "this dentry is stale" where it meant "this name
+ * is not here for you", and the VFS acts on stale globally: d_invalidate()
+ * detaches every mount on that dentry in every mount namespace.  One lookup
+ * from inside a Container took the host's bpffs and tracefs with it.
+ *
+ * The tmpfs is mounted in the test's own mount namespace, so the machine
+ * running this cannot lose a mount it needs, while the dentry the mount hangs
+ * on is still the shared one the bug worked through.
+ */
+TEST_F(ve_perms, hidden_entry_keeps_its_mount)
+{
+	char path[PATH_MAX];
+	int status;
+	pid_t pid;
+
+	if (!entry_present(variant->dir_prefix, variant->dir))
+		SKIP(return, "%s/%s absent", variant->dir_prefix, variant->dir);
+	snprintf(path, sizeof(path), "%s/%s", variant->dir_prefix, variant->dir);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		if (unshare(CLONE_NEWNS) != 0 ||
+		    mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) != 0 ||
+		    mount("ve_selftest", path, "tmpfs", 0, NULL) != 0)
+			_exit(255);
+		if (is_mounted(path) != 1)
+			_exit(254);
+
+		/*
+		 * The lookup that used to unmount it.  What the VE is told
+		 * depends on the filesystem and on the mount now covering the
+		 * entry, and enforce_denies() already covers that.  Here only
+		 * the mount surviving the lookup is the point.
+		 */
+		ve_open_rel(self->cgv2_fd, self->ctid_a, variant->dir_prefix,
+			    variant->dir, O_RDONLY | O_DIRECTORY);
+
+		_exit(is_mounted(path) == 1 ? 0 : 2);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	ASSERT_TRUE(WIFEXITED(status));
+	if (WEXITSTATUS(status) == 2)
+		TH_LOG("the VE lookup unmounted %s", path);
+	EXPECT_EQ(WEXITSTATUS(status), 0);
+}
+
 TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index 69c0a52dd7ef..83ace9e6db42 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -1,8 +1,8 @@
 /* SPDX-License-Identifier: GPL-2.0 */
 /*
  * Shared helpers for the ve selftests: a private cgroup2 mount, small file and
- * cgroup helpers, and VE cgroup create and destroy, used across the tests in
- * this directory.
+ * cgroup helpers, VE cgroup create and destroy, and a mount point query, used
+ * across the tests in this directory.
  */
 #ifndef __SELFTESTS_VE_VE_SELFTEST_H
 #define __SELFTESTS_VE_VE_SELFTEST_H
@@ -180,4 +180,27 @@ static inline void destroy_ve(int cgv2_fd, int id)
 		__func__, id, strerror(errno));
 }
 
+/* Is @path a mount point in this task's mount namespace? */
+static inline int is_mounted(const char *path)
+{
+	char line[PATH_MAX + 128], target[PATH_MAX];
+	int found = 0;
+	unsigned int u;
+	FILE *f;
+
+	f = fopen("/proc/self/mountinfo", "r");
+	if (!f)
+		return -1;
+	while (fgets(line, sizeof(line), f)) {
+		if (sscanf(line, "%u %u %u:%u %*s %s", &u, &u, &u, &u, target) != 5)
+			continue;
+		if (strcmp(target, path) == 0) {
+			found = 1;
+			break;
+		}
+	}
+	fclose(f);
+	return found;
+}
+
 #endif /* __SELFTESTS_VE_VE_SELFTEST_H */
-- 
2.43.0


^ permalink raw reply	[relevance 5%]

* [Devel] [PATCH RHEL10 COMMIT] drivers/vhost/blk: rework queue/backend setup
  2026-08-25 12:51  7% ` [Devel] [PATCH VZ10 v3 5/5] drivers/vhost/blk: rework queue/backend setup Andrey Zhadchenko
@ 2026-08-25 13:57  7%   ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-25 13:57 UTC (permalink / raw)


The commit is pushed to "branch-rh10-6.12.0-211.39.1.16.x.vz10-ovz" and will appear at git at bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.39.1.16.9.vz10
------>
commit 95d39b7ffa50047eadf53f48419e8cea84a175ea
Author: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
Date:   Tue Aug 25 15:51:52 2026 +0300

    drivers/vhost/blk: rework queue/backend setup
    
    vhost_blk_setup() is pretty bad: silently refusing changed vq->num
    if requests are already allocated, fetching user input second time
    (double-fetch vulnerability).
    To handle this, tie request allocation to backend existence. After
    all, if there is no backend, there is no point in having requests.
    Also expand it to get rid of boilerplate drop_backend, flush,
    fput sequence in a few places.
    
    https://virtuozzo.atlassian.net/browse/VSTOR-138640
    Fixes: d8722ff88c5d ("drivers/vhost: vhost-blk accelerator for virtio-blk guests")
    Feature: vhost-blk: in-kernel accelerator for virtio-blk guests
    Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
    Reviewed-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
    Reviewed-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 drivers/vhost/blk.c | 125 ++++++++++++++++++++++------------------------------
 1 file changed, 53 insertions(+), 72 deletions(-)

diff --git a/drivers/vhost/blk.c b/drivers/vhost/blk.c
index b2cf111bbb455..fbdd04f5a7e28 100644
--- a/drivers/vhost/blk.c
+++ b/drivers/vhost/blk.c
@@ -658,11 +658,14 @@ static void vhost_blk_flush(struct vhost_blk *blk)
 	spin_unlock(&blk->flush_lock);
 }
 
-static inline void vhost_blk_drop_backends(struct vhost_blk *blk)
+static void vhost_blk_drop_backend(struct vhost_blk *blk)
 {
 	struct vhost_virtqueue *vq;
 	int i;
 
+	if (!blk->backend)
+		return;
+
 	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
 		vq = &blk->vqs[i].vq;
 
@@ -670,6 +673,43 @@ static inline void vhost_blk_drop_backends(struct vhost_blk *blk)
 		vhost_vq_set_backend(vq, NULL);
 		mutex_unlock(&vq->mutex);
 	}
+
+	vhost_blk_flush(blk);
+
+	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
+		kvfree(blk->vqs[i].req);
+		blk->vqs[i].req = NULL;
+	}
+
+	fput(blk->backend);
+	blk->backend = NULL;
+}
+
+static int vhost_blk_setup_vqs(struct vhost_blk *blk)
+{
+	struct vhost_virtqueue *vq;
+	int ret, i;
+
+	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
+		vq = &blk->vqs[i].vq;
+
+		if (!vhost_vq_is_setup(vq))
+			continue;
+
+		blk->vqs[i].req = kvmalloc_array(vq->num, sizeof(struct vhost_blk_req),
+						 GFP_KERNEL);
+		if (!blk->vqs[i].req)
+			return -ENOMEM;
+
+		mutex_lock(&vq->mutex);
+		vhost_vq_set_backend(vq, blk->backend);
+		ret = vhost_vq_init_access(vq);
+		mutex_unlock(&vq->mutex);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
 }
 
 static int vhost_blk_open(struct inode *inode, struct file *file)
@@ -722,16 +762,10 @@ static int vhost_blk_open(struct inode *inode, struct file *file)
 static int vhost_blk_release(struct inode *inode, struct file *f)
 {
 	struct vhost_blk *blk = f->private_data;
-	int i;
 
-	vhost_blk_drop_backends(blk);
-	vhost_blk_flush(blk);
+	vhost_blk_drop_backend(blk);
 	vhost_dev_stop(&blk->dev);
-	if (blk->backend)
-		fput(blk->backend);
 	vhost_dev_cleanup(&blk->dev);
-	for (i = 0; i < VHOST_BLK_VQ_MAX; i++)
-		kvfree(blk->vqs[i].req);
 	kfree(blk->dev.vqs);
 	kvfree(blk);
 
@@ -765,32 +799,19 @@ static int vhost_blk_set_features(struct vhost_blk *blk, u64 features)
 
 static long vhost_blk_set_backend(struct vhost_blk *blk, int fd)
 {
-	struct vhost_virtqueue *vq;
 	struct file *file;
 	struct inode *inode;
-	int ret, i;
+	int ret;
 
 	mutex_lock(&blk->dev.mutex);
 	ret = vhost_dev_check_owner(&blk->dev);
 	if (ret)
 		goto out_dev;
 
-	/*
-	 * fd < 0 means "stop the device".  Detach the backend from every vq so
-	 * vhost_blk_handle_guest_kick() stops fetching descriptors, drain the
-	 * in-flight requests, and release the backing file.
-	 */
+	/* fd < 0 means "stop the device" */
 	if (fd < 0) {
-		if (!blk->backend) {
-			ret = 0;		/* already stopped */
-			goto out_dev;
-		}
-		vhost_blk_drop_backends(blk);
-		vhost_blk_flush(blk);
-		fput(blk->backend);
-		blk->backend = NULL;
 		ret = 0;
-		goto out_dev;
+		goto out_drop;
 	}
 
 	if (blk->backend) {
@@ -807,31 +828,20 @@ static long vhost_blk_set_backend(struct vhost_blk *blk, int fd)
 	inode = file->f_mapping->host;
 	if (!S_ISBLK(inode->i_mode)) {
 		ret = -EFAULT;
-		goto out_file;
-	}
-
-	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
-		vq = &blk->vqs[i].vq;
-		if (!vhost_vq_access_ok(vq)) {
-			ret = -EFAULT;
-			goto out_drop;
-		}
-
-		mutex_lock(&vq->mutex);
-		vhost_vq_set_backend(vq, file);
-		ret = vhost_vq_init_access(vq);
-		mutex_unlock(&vq->mutex);
+		fput(file);
+		goto out_dev;
 	}
 
 	blk->backend = file;
+	ret = vhost_blk_setup_vqs(blk);
+	if (ret)
+		goto out_drop;
 
 	mutex_unlock(&blk->dev.mutex);
 	return 0;
 
 out_drop:
-	vhost_blk_drop_backends(blk);
-out_file:
-	fput(file);
+	vhost_blk_drop_backend(blk);
 out_dev:
 	mutex_unlock(&blk->dev.mutex);
 	return ret;
@@ -840,7 +850,7 @@ static long vhost_blk_set_backend(struct vhost_blk *blk, int fd)
 static long vhost_blk_reset_owner(struct vhost_blk *blk)
 {
 	struct vhost_iotlb *umem;
-	int err, i;
+	int err;
 
 	mutex_lock(&blk->dev.mutex);
 	err = vhost_dev_check_owner(&blk->dev);
@@ -851,42 +861,15 @@ static long vhost_blk_reset_owner(struct vhost_blk *blk)
 		err = -ENOMEM;
 		goto done;
 	}
-	vhost_blk_drop_backends(blk);
-	if (blk->backend) {
-		fput(blk->backend);
-		blk->backend = NULL;
-	}
-	vhost_blk_flush(blk);
+	vhost_blk_drop_backend(blk);
 	vhost_dev_stop(&blk->dev);
 	vhost_dev_reset_owner(&blk->dev, umem);
 
-	for (i = 0; i < VHOST_BLK_VQ_MAX; i++) {
-		kvfree(blk->vqs[i].req);
-		blk->vqs[i].req = NULL;
-	}
-
 done:
 	mutex_unlock(&blk->dev.mutex);
 	return err;
 }
 
-static int vhost_blk_setup(struct vhost_blk *blk, void __user *argp)
-{
-	struct vhost_vring_state s;
-
-	if (copy_from_user(&s, argp, sizeof(s)))
-		return -EFAULT;
-
-	if (blk->vqs[s.index].req)
-		return 0;
-
-	blk->vqs[s.index].req = kvmalloc(sizeof(struct vhost_blk_req) * s.num, GFP_KERNEL);
-	if (!blk->vqs[s.index].req)
-		return -ENOMEM;
-
-	return 0;
-}
-
 static long vhost_blk_ioctl(struct file *f, unsigned int ioctl,
 			    unsigned long arg)
 {
@@ -924,8 +907,6 @@ static long vhost_blk_ioctl(struct file *f, unsigned int ioctl,
 		ret = vhost_dev_ioctl(&blk->dev, ioctl, argp);
 		if (ret == -ENOIOCTLCMD)
 			ret = vhost_vring_ioctl(&blk->dev, ioctl, argp);
-		if (!ret && ioctl == VHOST_SET_VRING_NUM)
-			ret = vhost_blk_setup(blk, argp);
 		vhost_blk_flush(blk);
 		mutex_unlock(&blk->dev.mutex);
 		return ret;

^ permalink raw reply	[relevance 7%]

* Re: [Devel] [PATCH vz10 2/2] selftests/ve: check that hiding an entry does not unmount it
  2026-08-25 13:29  5% ` [Devel] [PATCH vz10 2/2] selftests/ve: check that hiding an entry does not unmount it Mirian Shilakadze
@ 2026-08-25 15:40  0%   ` Pavel Tikhomirov
  0 siblings, 0 replies; 200+ results
From: Pavel Tikhomirov @ 2026-08-25 15:40 UTC (permalink / raw)




On 8/25/26 15:29, Mirian Shilakadze wrote:
> kernfs_dop_revalidate() answered the per VE visibility check with the same
> "return 0" the staleness checks use, and the VFS reads 0 as a global fact:
> d_invalidate() hands every mountpoint under that dentry to
> __detach_mounts(), whose mountpoint hash is not scoped to a mount
> namespace.  A single lookup from inside a Container unmounted the host's
> bpffs, and libvzctl needs bpffs for the cgroup v2 device controller, so
> the whole node stopped being manageable.
> 
> Mount a tmpfs on the entry the variant already keeps host only, look it up
> from inside a VE, and require both that the VE is told ENOENT and that the
> mount is still there afterwards.  The mount is made in the test's own
> mount namespace so the machine running the test cannot lose a mount it
> needs, while the dentry the mount hangs on is still the shared one the bug
> worked through.
> 
> Fails without the preceding fix, on both the sysfs and the proc variant.
> 
> Feature: kernfs: per-CT entries visibility and permissions configuration
> https://virtuozzo.atlassian.net/browse/VSTOR-142552
> Signed-off-by: Mirian Shilakadze <mirian.shilakadze@virtuozzo.com>
> ---
>  tools/testing/selftests/ve/ve_perms_test.c | 52 ++++++++++++++++++++++
>  tools/testing/selftests/ve/ve_selftest.h   | 27 ++++++++++-
>  2 files changed, 77 insertions(+), 2 deletions(-)
> 
> diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
> index 4522950c17f2..25ffbd42c380 100644
> --- a/tools/testing/selftests/ve/ve_perms_test.c
> +++ b/tools/testing/selftests/ve/ve_perms_test.c
> @@ -24,6 +24,7 @@
>  #include <unistd.h>
>  #include <fcntl.h>
>  #include <limits.h>
> +#include <sys/mount.h>
>  #include <sys/wait.h>
>  #include <errno.h>
>  
> @@ -412,4 +413,55 @@ TEST_F(ve_perms, enforce_denies)
>  			      absent, O_RDONLY), EACCES);
>  }
>  
> +/*
> + * Looking up an entry that a VE cannot see must not disturb a mount that
> + * sits on it.
> + *
> + * The lookup used to answer "this dentry is stale" where it meant "this name
> + * is not here for you", and the VFS acts on stale globally: d_invalidate()
> + * detaches every mount on that dentry in every mount namespace.  One lookup
> + * from inside a Container took the host's bpffs and tracefs with it.
> + *
> + * The tmpfs is mounted in the test's own mount namespace, so the machine
> + * running this cannot lose a mount it needs, while the dentry the mount hangs
> + * on is still the shared one the bug worked through.
> + */
> +TEST_F(ve_perms, hidden_entry_keeps_its_mount)
> +{
> +	char path[PATH_MAX];
> +	int status;
> +	pid_t pid;
> +
> +	if (!entry_present(variant->dir_prefix, variant->dir))
> +		SKIP(return, "%s/%s absent", variant->dir_prefix, variant->dir);
> +	snprintf(path, sizeof(path), "%s/%s", variant->dir_prefix, variant->dir);
> +
> +	pid = fork();
> +	ASSERT_GE(pid, 0);
> +	if (pid == 0) {
> +		if (unshare(CLONE_NEWNS) != 0 ||
> +		    mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) != 0 ||
> +		    mount("ve_selftest", path, "tmpfs", 0, NULL) != 0)
> +			_exit(255);
> +		if (is_mounted(path) != 1)
> +			_exit(254);
> +
> +		/*
> +		 * The lookup that used to unmount it.  What the VE is told
> +		 * depends on the filesystem and on the mount now covering the
> +		 * entry, and enforce_denies() already covers that.  Here only
> +		 * the mount surviving the lookup is the point.
> +		 */
> +		ve_open_rel(self->cgv2_fd, self->ctid_a, variant->dir_prefix,
> +			    variant->dir, O_RDONLY | O_DIRECTORY);
> +
> +		_exit(is_mounted(path) == 1 ? 0 : 2);
> +	}
> +	ASSERT_EQ(waitpid(pid, &status, 0), pid);
> +	ASSERT_TRUE(WIFEXITED(status));
> +	if (WEXITSTATUS(status) == 2)
> +		TH_LOG("the VE lookup unmounted %s", path);
> +	EXPECT_EQ(WEXITSTATUS(status), 0);
> +}
> +
>  TEST_HARNESS_MAIN
> diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
> index 69c0a52dd7ef..83ace9e6db42 100644
> --- a/tools/testing/selftests/ve/ve_selftest.h
> +++ b/tools/testing/selftests/ve/ve_selftest.h
> @@ -1,8 +1,8 @@
>  /* SPDX-License-Identifier: GPL-2.0 */
>  /*
>   * Shared helpers for the ve selftests: a private cgroup2 mount, small file and
> - * cgroup helpers, and VE cgroup create and destroy, used across the tests in
> - * this directory.
> + * cgroup helpers, VE cgroup create and destroy, and a mount point query, used
> + * across the tests in this directory.
>   */
>  #ifndef __SELFTESTS_VE_VE_SELFTEST_H
>  #define __SELFTESTS_VE_VE_SELFTEST_H
> @@ -180,4 +180,27 @@ static inline void destroy_ve(int cgv2_fd, int id)
>  		__func__, id, strerror(errno));
>  }
>  
> +/* Is @path a mount point in this task's mount namespace? */
> +static inline int is_mounted(const char *path)
> +{
> +	char line[PATH_MAX + 128], target[PATH_MAX];
> +	int found = 0;
> +	unsigned int u;
> +	FILE *f;
> +
> +	f = fopen("/proc/self/mountinfo", "r");
> +	if (!f)
> +		return -1;
> +	while (fgets(line, sizeof(line), f)) {
> +		if (sscanf(line, "%u %u %u:%u %*s %s", &u, &u, &u, &u, target) != 5)
> +			continue;
> +		if (strcmp(target, path) == 0) {
> +			found = 1;
> +			break;
> +		}
> +	}

Take a look at openat2 RESOLVE_NO_XDEV, it detects the crossing of mount boundary
(open parent, open child from parent), and avoids heavy mountinfo reading.

> +	fclose(f);
> +	return found;
> +}
> +
>  #endif /* __SELFTESTS_VE_VE_SELFTEST_H */

-- 
Best regards, Pavel Tikhomirov
Senior Software Developer, Virtuozzo.


^ permalink raw reply	[relevance 0%]

* [Devel] [PATCH vz10 v2 2/2] selftests/ve: check that hiding an entry does not unmount it
  @ 2026-08-26 11:04  5% ` Mirian Shilakadze
  2026-08-26 16:15  5%   ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
  0 siblings, 1 reply; 200+ results
From: Mirian Shilakadze @ 2026-08-26 11:04 UTC (permalink / raw)
  To: khorenko, ptikhomirov; +Cc: devel

kernfs_dop_revalidate() answered the per VE visibility check with the same
"return 0" the staleness checks use, and the VFS reads 0 as a global fact:
d_invalidate() hands every mountpoint under that dentry to
__detach_mounts(), whose mountpoint hash is not scoped to a mount
namespace.  A single lookup from inside a Container unmounted the host's
bpffs, and libvzctl needs bpffs for the cgroup v2 device controller, so
the whole node stopped being manageable.

Mount a tmpfs on the entry the variant already keeps host only, look it up
from inside a VE, and require both that the VE is told ENOENT and that the
mount is still there afterwards.  The mount is made in the test's own
mount namespace so the machine running the test cannot lose a mount it
needs, while the dentry the mount hangs on is still the shared one the bug
worked through.

The mount check uses openat2() with RESOLVE_NO_XDEV, which fails with
EXDEV when the final component is a mount point, rather than reading
/proc/self/mountinfo.

Fails without the preceding fix, on both the sysfs and the proc variant.

Feature: kernfs: per-CT entries visibility and permissions configuration
https://virtuozzo.atlassian.net/browse/VSTOR-142552
Signed-off-by: Mirian Shilakadze <mirian.shilakadze@virtuozzo.com>
---
 tools/testing/selftests/ve/ve_perms_test.c | 52 ++++++++++++++++++++++
 tools/testing/selftests/ve/ve_selftest.h   | 40 ++++++++++++++++-
 2 files changed, 90 insertions(+), 2 deletions(-)

diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
index 4522950c17f2..25ffbd42c380 100644
--- a/tools/testing/selftests/ve/ve_perms_test.c
+++ b/tools/testing/selftests/ve/ve_perms_test.c
@@ -24,6 +24,7 @@
 #include <unistd.h>
 #include <fcntl.h>
 #include <limits.h>
+#include <sys/mount.h>
 #include <sys/wait.h>
 #include <errno.h>
 
@@ -412,4 +413,55 @@ TEST_F(ve_perms, enforce_denies)
 			      absent, O_RDONLY), EACCES);
 }
 
+/*
+ * Looking up an entry that a VE cannot see must not disturb a mount that
+ * sits on it.
+ *
+ * The lookup used to answer "this dentry is stale" where it meant "this name
+ * is not here for you", and the VFS acts on stale globally: d_invalidate()
+ * detaches every mount on that dentry in every mount namespace.  One lookup
+ * from inside a Container took the host's bpffs and tracefs with it.
+ *
+ * The tmpfs is mounted in the test's own mount namespace, so the machine
+ * running this cannot lose a mount it needs, while the dentry the mount hangs
+ * on is still the shared one the bug worked through.
+ */
+TEST_F(ve_perms, hidden_entry_keeps_its_mount)
+{
+	char path[PATH_MAX];
+	int status;
+	pid_t pid;
+
+	if (!entry_present(variant->dir_prefix, variant->dir))
+		SKIP(return, "%s/%s absent", variant->dir_prefix, variant->dir);
+	snprintf(path, sizeof(path), "%s/%s", variant->dir_prefix, variant->dir);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		if (unshare(CLONE_NEWNS) != 0 ||
+		    mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) != 0 ||
+		    mount("ve_selftest", path, "tmpfs", 0, NULL) != 0)
+			_exit(255);
+		if (is_mounted(path) != 1)
+			_exit(254);
+
+		/*
+		 * The lookup that used to unmount it.  What the VE is told
+		 * depends on the filesystem and on the mount now covering the
+		 * entry, and enforce_denies() already covers that.  Here only
+		 * the mount surviving the lookup is the point.
+		 */
+		ve_open_rel(self->cgv2_fd, self->ctid_a, variant->dir_prefix,
+			    variant->dir, O_RDONLY | O_DIRECTORY);
+
+		_exit(is_mounted(path) == 1 ? 0 : 2);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	ASSERT_TRUE(WIFEXITED(status));
+	if (WEXITSTATUS(status) == 2)
+		TH_LOG("the VE lookup unmounted %s", path);
+	EXPECT_EQ(WEXITSTATUS(status), 0);
+}
+
 TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index 69c0a52dd7ef..c53bf7900d20 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -1,8 +1,8 @@
 /* SPDX-License-Identifier: GPL-2.0 */
 /*
  * Shared helpers for the ve selftests: a private cgroup2 mount, small file and
- * cgroup helpers, and VE cgroup create and destroy, used across the tests in
- * this directory.
+ * cgroup helpers, VE cgroup create and destroy, and a mount point query, used
+ * across the tests in this directory.
  */
 #ifndef __SELFTESTS_VE_VE_SELFTEST_H
 #define __SELFTESTS_VE_VE_SELFTEST_H
@@ -16,6 +16,8 @@
 #include <limits.h>
 #include <sys/stat.h>
 #include <sys/mount.h>
+#include <sys/syscall.h>
+#include <linux/openat2.h>
 
 #ifndef CLONE_NEWVE
 #define CLONE_NEWVE		0x00000040
@@ -180,4 +182,38 @@ static inline void destroy_ve(int cgv2_fd, int id)
 		__func__, id, strerror(errno));
 }
 
+/*
+ * Is @path a mount point?  RESOLVE_NO_XDEV makes openat2() fail with EXDEV
+ * when the final component is a mount point, which answers the question
+ * without reading the mount table.
+ */
+static inline int is_mounted(const char *path)
+{
+	struct open_how how = {
+		.flags = O_PATH | O_CLOEXEC,
+		.resolve = RESOLVE_NO_XDEV,
+	};
+	char buf[PATH_MAX], *dir, *base;
+	int dfd, fd;
+
+	if (snprintf(buf, sizeof(buf), "%s", path) >= (int)sizeof(buf))
+		return -1;
+	base = strrchr(buf, '/');
+	if (!base)
+		return -1;
+	*base++ = '\0';
+	dir = buf[0] ? buf : "/";
+
+	dfd = open(dir, O_PATH | O_DIRECTORY | O_CLOEXEC);
+	if (dfd < 0)
+		return -1;
+	fd = syscall(__NR_openat2, dfd, base, &how, sizeof(how));
+	close(dfd);
+	if (fd >= 0) {
+		close(fd);
+		return 0;
+	}
+	return errno == EXDEV ? 1 : -1;
+}
+
 #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 5%]

* [Devel] [PATCH VZ10] b4: ship project defaults in .b4-config
@ 2026-08-26 14:44  5% Vasileios Almpanis
  2026-08-26 15:15  0% ` Pavel Tikhomirov
  2026-08-26 16:42  5% ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
  0 siblings, 2 replies; 200+ results
From: Vasileios Almpanis @ 2026-08-26 14:44 UTC (permalink / raw)
  To: Pavel Tikhomirov, Konstantin Khorenko; +Cc: devel

b4 fetches series from lore.kernel.org by default. To use it with our
list archive at lore.virtuozzo.com every developer had to set the same
handful of b4.* git config keys, plus two more to get the right
recipients on "b4 send" and one for the subject prefix.

b4 loads a .b4-config at the top of the tree as defaults for commands
run inside it, so put the archive URL masks, the default recipients and
the VZ10 subject prefix there. Add the file to the .gitignore dot-file
exceptions so it is tracked.

https://virtuozzo.atlassian.net/browse/VSTOR-142960
Signed-off-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>

Feature: internal
---
Signed-off-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
---
 .b4-config | 7 +++++++
 .gitignore | 1 +
 2 files changed, 8 insertions(+)

diff --git a/.b4-config b/.b4-config
new file mode 100644
index 000000000000..3f5aac9bb1cc
--- /dev/null
+++ b/.b4-config
@@ -0,0 +1,7 @@
+[b4]
+	midmask = http://lore.virtuozzo.com/%s
+	searchmask = http://lore.virtuozzo.com/all/?x=m&t=1&q=%s
+	linkmask = http://lore.virtuozzo.com/%s
+	send-series-to = "Konstantin Khorenko <khorenko@virtuozzo.com>"
+	send-series-cc = devel@openvz.org
+	send-prefixes = VZ10
diff --git a/.gitignore b/.gitignore
index 0c7ad0516fc6..b64c07bd537e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -103,6 +103,7 @@ modules.order
 #
 # We don't want to ignore the following even if they are dot-files
 #
+!.b4-config
 !.clang-format
 !.cocciconfig
 !.editorconfig

---
base-commit: 4305092314bbfbc90cb26f9ec8c6bf78c4513f04
change-id: 20260826-b4-setup-9a776f3efccd

Best regards,
--  
Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>

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

^ permalink raw reply	[relevance 5%]

* Re: [Devel] [PATCH VZ10] b4: ship project defaults in .b4-config
  2026-08-26 14:44  5% [Devel] [PATCH VZ10] b4: ship project defaults in .b4-config Vasileios Almpanis
@ 2026-08-26 15:15  0% ` Pavel Tikhomirov
  2026-08-26 16:42  5% ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Pavel Tikhomirov @ 2026-08-26 15:15 UTC (permalink / raw)
  To: Vasileios Almpanis, Konstantin Khorenko; +Cc: devel

Works for me.

Reviewed-by: Pavel Tikhomirov <ptikhomirov@viruozzo.com>

On 8/26/26 16:44, Vasileios Almpanis wrote:
> b4 fetches series from lore.kernel.org by default. To use it with our
> list archive at lore.virtuozzo.com every developer had to set the same
> handful of b4.* git config keys, plus two more to get the right
> recipients on "b4 send" and one for the subject prefix.
> 
> b4 loads a .b4-config at the top of the tree as defaults for commands
> run inside it, so put the archive URL masks, the default recipients and
> the VZ10 subject prefix there. Add the file to the .gitignore dot-file
> exceptions so it is tracked.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-142960
> Signed-off-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
> 
> Feature: internal
> ---
> Signed-off-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
> ---
>  .b4-config | 7 +++++++
>  .gitignore | 1 +
>  2 files changed, 8 insertions(+)
> 
> diff --git a/.b4-config b/.b4-config
> new file mode 100644
> index 000000000000..3f5aac9bb1cc
> --- /dev/null
> +++ b/.b4-config
> @@ -0,0 +1,7 @@
> +[b4]
> +	midmask = http://lore.virtuozzo.com/%s
> +	searchmask = http://lore.virtuozzo.com/all/?x=m&t=1&q=%s
> +	linkmask = http://lore.virtuozzo.com/%s
> +	send-series-to = "Konstantin Khorenko <khorenko@virtuozzo.com>"
> +	send-series-cc = devel@openvz.org
> +	send-prefixes = VZ10
> diff --git a/.gitignore b/.gitignore
> index 0c7ad0516fc6..b64c07bd537e 100644
> --- a/.gitignore
> +++ b/.gitignore
> @@ -103,6 +103,7 @@ modules.order
>  #
>  # We don't want to ignore the following even if they are dot-files
>  #
> +!.b4-config
>  !.clang-format
>  !.cocciconfig
>  !.editorconfig
> 
> ---
> base-commit: 4305092314bbfbc90cb26f9ec8c6bf78c4513f04
> change-id: 20260826-b4-setup-9a776f3efccd
> 
> Best regards,
> --  
> Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
> 

-- 
Best regards, Pavel Tikhomirov
Senior Software Developer, Virtuozzo.

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

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH RHEL10 COMMIT] ve/fs: unlink the mount namespace on the copy_mnt_ns() error path
  2026-08-17  7:16  4% ` [Devel] [PATCH vz10 1/3] ve/fs: unlink the mount namespace on the copy_mnt_ns() error path Mirian Shilakadze
@ 2026-08-26 15:38  4%   ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-26 15:38 UTC (permalink / raw)
  To: Mirian Shilakadze; +Cc: OpenVZ devel

The commit is pushed to "branch-rh10-6.12.0-211.39.1.16.x.vz10-ovz" and will appear at git@bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.39.1.16.10.vz10
------>
commit 5e3e3c3facbcd29dfa49f7f6ab5aabb39a60d1f9
Author: Mirian Shilakadze <mirian.shilakadze@virtuozzo.com>
Date:   Mon Aug 17 11:16:39 2026 +0400

    ve/fs: unlink the mount namespace on the copy_mnt_ns() error path
    
    alloc_mnt_ns() does two things upstream does not: it links the namespace
    onto all_mntns_list and takes a reference on its owning VE. Both are
    undone in free_mnt_ns(), which holds the only list_del() of mntns_list
    and the only put_ve(ns->ve_owner) in the file.
    
    copy_mnt_ns() does not call it when copy_tree() fails. It open codes the
    teardown and finishes with mnt_ns_release(), which drops the passive
    count and kfree()s the namespace without unlinking it, so the namespace
    is freed while all_mntns_list still points at it and the VE reference is
    leaked. The next namespace creation runs list_add_tail() through the
    dangling entry.
    
    A container reaches this deterministically. alloc_vfsmnt() returns NULL
    when !ve_mount_allowed(), that is when the VE is at sysctl_ve_mount_nr,
    clone_mnt() turns that into -ENOMEM and copy_tree() propagates it. So a
    container sitting at its own mount limit that calls unshare(CLONE_NEWNS)
    takes the error path every time, with no memory pressure and nothing
    beyond CAP_SYS_ADMIN in its own user namespace, and panics the host:
    
      list_add corruption. prev->next should be next (ffffffffa9ca77f0), but
      was ff2834a3cdfaeed0. (prev=ff2834a3cdfaeed0).
      kernel BUG at lib/list_debug.c:32!
      CPU: 94 UID: 0 PID: 7139 Comm: unshare ve: 900
       alloc_mnt_ns+0xd5/0x210
       copy_mnt_ns+0x82/0x3c0
       create_new_namespaces+0x5d/0x2f0
       unshare_nsproxy_namespaces+0x69/0xc0
       ksys_unshare+0x213/0x3f0
    
    prev->next == prev is INIT_LIST_HEAD() on reallocated memory, the freed
    namespace reused while the list still referenced it. CONFIG_DEBUG_LIST is
    only what makes it a clean BUG, without it the same list_add_tail()
    writes through the dangling pointer silently.
    
    The path used to call free_mnt_ns() and was correct. Upstream replaced
    that with the open coded sequence because free_mnt_ns() reaches
    mnt_ns_tree_remove(), which rb_erase()s a node that copy_mnt_ns() has not
    inserted yet, mnt_ns_tree_add() running only after the copy succeeds.
    Upstream is unaffected by the replacement because its free_mnt_ns()
    carries nothing else. Ours does.
    
    So do not restore the free_mnt_ns() call, that would reintroduce the
    rb_erase() upstream fixed. Split the part that is ours into
    mnt_ns_unlink() and call it from both places, so a future addition to
    namespace teardown has one home rather than two that can drift apart,
    which is how this happened.
    
    Fixes: 229fd15908fe ("fs: don't try and remove empty rbtree node")
    Fixes: 1db60e545f65 ("ve/mntns: add ve_owner to struct mnt_namespace")
    https://virtuozzo.atlassian.net/browse/VSTOR-141545
    Feature: ve: ve generic structures
    Signed-off-by: Mirian Shilakadze <mirian.shilakadze@virtuozzo.com>
    Reviewed-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
    Reviewed-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 fs/namespace.c | 24 +++++++++++++++++++-----
 1 file changed, 19 insertions(+), 5 deletions(-)

diff --git a/fs/namespace.c b/fs/namespace.c
index 4d4dc52903508..7e27537dcdaf9 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -4243,17 +4243,30 @@ static void dec_mnt_namespaces(struct ucounts *ucounts)
 static LIST_HEAD(all_mntns_list);
 static DEFINE_SPINLOCK(all_mntns_list_lock);
 
-static void free_mnt_ns(struct mnt_namespace *ns)
+/*
+ * Undo the bookkeeping alloc_mnt_ns() sets up beyond what upstream does: the
+ * entry on all_mntns_list and the reference on the owning VE.
+ *
+ * Kept separate from free_mnt_ns() because copy_mnt_ns() has to unwind a
+ * namespace that is not in mnt_ns_tree yet, so it cannot use free_mnt_ns()
+ * without rb_erase()ing a node that was never inserted.
+ */
+static void mnt_ns_unlink(struct mnt_namespace *ns)
 {
-	if (!is_anon_ns(ns))
-		ns_free_inum(&ns->ns);
-	dec_mnt_namespaces(ns->ucounts);
-
 	spin_lock(&all_mntns_list_lock);
 	list_del(&ns->mntns_list);
 	spin_unlock(&all_mntns_list_lock);
 
 	put_ve(ns->ve_owner);
+}
+
+static void free_mnt_ns(struct mnt_namespace *ns)
+{
+	if (!is_anon_ns(ns))
+		ns_free_inum(&ns->ns);
+	dec_mnt_namespaces(ns->ucounts);
+
+	mnt_ns_unlink(ns);
 
 	mnt_ns_tree_remove(ns);
 }
@@ -4347,6 +4360,7 @@ struct mnt_namespace *copy_mnt_ns(unsigned long flags, struct mnt_namespace *ns,
 		namespace_unlock();
 		ns_free_inum(&new_ns->ns);
 		dec_mnt_namespaces(new_ns->ucounts);
+		mnt_ns_unlink(new_ns);
 		mnt_ns_release(new_ns);
 		return ERR_CAST(new);
 	}
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* Re: [Devel] [PATCH RHEL10 COMMIT] selftests/ve: check that hiding an entry does not unmount it
  2026-08-26 11:04  5% ` [Devel] [PATCH vz10 v2 2/2] selftests/ve: check that hiding an entry does not unmount it Mirian Shilakadze
@ 2026-08-26 16:15  5%   ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-26 16:15 UTC (permalink / raw)
  To: Mirian Shilakadze; +Cc: OpenVZ devel

The commit is pushed to "branch-rh10-6.12.0-211.39.1.16.x.vz10-ovz" and will appear at git@bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.39.1.16.10.vz10
------>
commit e6d9a8ea97c1933241869f60ac5677865d848dab
Author: Mirian Shilakadze <mirian.shilakadze@virtuozzo.com>
Date:   Wed Aug 26 15:04:11 2026 +0400

    selftests/ve: check that hiding an entry does not unmount it
    
    kernfs_dop_revalidate() answered the per VE visibility check with the same
    "return 0" the staleness checks use, and the VFS reads 0 as a global fact:
    d_invalidate() hands every mountpoint under that dentry to
    __detach_mounts(), whose mountpoint hash is not scoped to a mount
    namespace.  A single lookup from inside a Container unmounted the host's
    bpffs, and libvzctl needs bpffs for the cgroup v2 device controller, so
    the whole node stopped being manageable.
    
    Mount a tmpfs on the entry the variant already keeps host only, look it up
    from inside a VE, and require both that the VE is told ENOENT and that the
    mount is still there afterwards.  The mount is made in the test's own
    mount namespace so the machine running the test cannot lose a mount it
    needs, while the dentry the mount hangs on is still the shared one the bug
    worked through.
    
    The mount check uses openat2() with RESOLVE_NO_XDEV, which fails with
    EXDEV when the final component is a mount point, rather than reading
    /proc/self/mountinfo.
    
    Fails without the preceding fix, on both the sysfs and the proc variant.
    
    Feature: kernfs: per-CT entries visibility and permissions configuration
    https://virtuozzo.atlassian.net/browse/VSTOR-142552
    Signed-off-by: Mirian Shilakadze <mirian.shilakadze@virtuozzo.com>
    Reviewed-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
    Reviewed-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 tools/testing/selftests/ve/ve_perms_test.c | 52 ++++++++++++++++++++++++++++++
 tools/testing/selftests/ve/ve_selftest.h   | 40 +++++++++++++++++++++--
 2 files changed, 90 insertions(+), 2 deletions(-)

diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
index 4522950c17f2f..25ffbd42c3802 100644
--- a/tools/testing/selftests/ve/ve_perms_test.c
+++ b/tools/testing/selftests/ve/ve_perms_test.c
@@ -24,6 +24,7 @@
 #include <unistd.h>
 #include <fcntl.h>
 #include <limits.h>
+#include <sys/mount.h>
 #include <sys/wait.h>
 #include <errno.h>
 
@@ -412,4 +413,55 @@ TEST_F(ve_perms, enforce_denies)
 			      absent, O_RDONLY), EACCES);
 }
 
+/*
+ * Looking up an entry that a VE cannot see must not disturb a mount that
+ * sits on it.
+ *
+ * The lookup used to answer "this dentry is stale" where it meant "this name
+ * is not here for you", and the VFS acts on stale globally: d_invalidate()
+ * detaches every mount on that dentry in every mount namespace.  One lookup
+ * from inside a Container took the host's bpffs and tracefs with it.
+ *
+ * The tmpfs is mounted in the test's own mount namespace, so the machine
+ * running this cannot lose a mount it needs, while the dentry the mount hangs
+ * on is still the shared one the bug worked through.
+ */
+TEST_F(ve_perms, hidden_entry_keeps_its_mount)
+{
+	char path[PATH_MAX];
+	int status;
+	pid_t pid;
+
+	if (!entry_present(variant->dir_prefix, variant->dir))
+		SKIP(return, "%s/%s absent", variant->dir_prefix, variant->dir);
+	snprintf(path, sizeof(path), "%s/%s", variant->dir_prefix, variant->dir);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		if (unshare(CLONE_NEWNS) != 0 ||
+		    mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) != 0 ||
+		    mount("ve_selftest", path, "tmpfs", 0, NULL) != 0)
+			_exit(255);
+		if (is_mounted(path) != 1)
+			_exit(254);
+
+		/*
+		 * The lookup that used to unmount it.  What the VE is told
+		 * depends on the filesystem and on the mount now covering the
+		 * entry, and enforce_denies() already covers that.  Here only
+		 * the mount surviving the lookup is the point.
+		 */
+		ve_open_rel(self->cgv2_fd, self->ctid_a, variant->dir_prefix,
+			    variant->dir, O_RDONLY | O_DIRECTORY);
+
+		_exit(is_mounted(path) == 1 ? 0 : 2);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	ASSERT_TRUE(WIFEXITED(status));
+	if (WEXITSTATUS(status) == 2)
+		TH_LOG("the VE lookup unmounted %s", path);
+	EXPECT_EQ(WEXITSTATUS(status), 0);
+}
+
 TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index 69c0a52dd7ef0..c53bf7900d208 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -1,8 +1,8 @@
 /* SPDX-License-Identifier: GPL-2.0 */
 /*
  * Shared helpers for the ve selftests: a private cgroup2 mount, small file and
- * cgroup helpers, and VE cgroup create and destroy, used across the tests in
- * this directory.
+ * cgroup helpers, VE cgroup create and destroy, and a mount point query, used
+ * across the tests in this directory.
  */
 #ifndef __SELFTESTS_VE_VE_SELFTEST_H
 #define __SELFTESTS_VE_VE_SELFTEST_H
@@ -16,6 +16,8 @@
 #include <limits.h>
 #include <sys/stat.h>
 #include <sys/mount.h>
+#include <sys/syscall.h>
+#include <linux/openat2.h>
 
 #ifndef CLONE_NEWVE
 #define CLONE_NEWVE		0x00000040
@@ -180,4 +182,38 @@ static inline void destroy_ve(int cgv2_fd, int id)
 		__func__, id, strerror(errno));
 }
 
+/*
+ * Is @path a mount point?  RESOLVE_NO_XDEV makes openat2() fail with EXDEV
+ * when the final component is a mount point, which answers the question
+ * without reading the mount table.
+ */
+static inline int is_mounted(const char *path)
+{
+	struct open_how how = {
+		.flags = O_PATH | O_CLOEXEC,
+		.resolve = RESOLVE_NO_XDEV,
+	};
+	char buf[PATH_MAX], *dir, *base;
+	int dfd, fd;
+
+	if (snprintf(buf, sizeof(buf), "%s", path) >= (int)sizeof(buf))
+		return -1;
+	base = strrchr(buf, '/');
+	if (!base)
+		return -1;
+	*base++ = '\0';
+	dir = buf[0] ? buf : "/";
+
+	dfd = open(dir, O_PATH | O_DIRECTORY | O_CLOEXEC);
+	if (dfd < 0)
+		return -1;
+	fd = syscall(__NR_openat2, dfd, base, &how, sizeof(how));
+	close(dfd);
+	if (fd >= 0) {
+		close(fd);
+		return 0;
+	}
+	return errno == EXDEV ? 1 : -1;
+}
+
 #endif /* __SELFTESTS_VE_VE_SELFTEST_H */
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 5%]

* Re: [Devel] [PATCH RHEL10 COMMIT] b4: ship project defaults in .b4-config
  2026-08-26 14:44  5% [Devel] [PATCH VZ10] b4: ship project defaults in .b4-config Vasileios Almpanis
  2026-08-26 15:15  0% ` Pavel Tikhomirov
@ 2026-08-26 16:42  5% ` Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-26 16:42 UTC (permalink / raw)
  To: Vasileios Almpanis; +Cc: OpenVZ devel

The commit is pushed to "branch-rh10-6.12.0-211.39.1.16.x.vz10-ovz" and will appear at git@bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.39.1.16.10.vz10
------>
commit 58367508a27f0be6127bf618fb1c974921d2b45e
Author: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
Date:   Wed Aug 26 14:44:10 2026 +0000

    b4: ship project defaults in .b4-config
    
    b4 fetches series from lore.kernel.org by default. To use it with our
    list archive at lore.virtuozzo.com every developer had to set the same
    handful of b4.* git config keys, plus two more to get the right
    recipients on "b4 send" and one for the subject prefix.
    
    b4 loads a .b4-config at the top of the tree as defaults for commands
    run inside it, so put the archive URL masks, the default recipients and
    the VZ10 subject prefix there. Add the file to the .gitignore dot-file
    exceptions so it is tracked.
    
    https://virtuozzo.atlassian.net/browse/VSTOR-142960
    Feature: internal
    Signed-off-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
    Reviewed-by: Pavel Tikhomirov <ptikhomirov@viruozzo.com>
---
 .b4-config | 7 +++++++
 .gitignore | 1 +
 2 files changed, 8 insertions(+)

diff --git a/.b4-config b/.b4-config
new file mode 100644
index 0000000000000..3f5aac9bb1cc4
--- /dev/null
+++ b/.b4-config
@@ -0,0 +1,7 @@
+[b4]
+	midmask = http://lore.virtuozzo.com/%s
+	searchmask = http://lore.virtuozzo.com/all/?x=m&t=1&q=%s
+	linkmask = http://lore.virtuozzo.com/%s
+	send-series-to = "Konstantin Khorenko <khorenko@virtuozzo.com>"
+	send-series-cc = devel@openvz.org
+	send-prefixes = VZ10
diff --git a/.gitignore b/.gitignore
index 0c7ad0516fc67..b64c07bd537ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -103,6 +103,7 @@ /pacman/
 #
 # We don't want to ignore the following even if they are dot-files
 #
+!.b4-config
 !.clang-format
 !.cocciconfig
 !.editorconfig
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 5%]

* Re: [Devel] [PATCH vz10 v2] ve: downgrade the trusted exec/mmap denial from WARN to pr_warn
  2026-08-19 16:25 13% ` [Devel] [PATCH vz10 v2] ve: downgrade the trusted exec/mmap denial from WARN to pr_warn Konstantin Khorenko
@ 2026-08-26 16:49  0%   ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-26 16:49 UTC (permalink / raw)
  To: Pavel Tikhomirov; +Cc: OpenVZ devel list

a kind ping

--
Best regards,

Konstantin Khorenko,
Virtuozzo Linux Kernel Team

On 8/19/26 18:25, Konstantin Khorenko wrote:
> ve_check_trusted_exec()/ve_check_trusted_mmap() protect the host, but
> the WARN(1, ...) is too much here:
> 
>  * it taints the kernel while this is not a kernel bug which we beed to
>    debug and fix
>  * the backtrace only shows the exec/mmap path, which is already known
>  * on a host booted with panic_on_warn a denied exec takes the host down
> 
> The SIGSEGV and the core dump of the offending process are enough to
> report and investigate such an attempt. Downgrade the WARN() to
> pr_warn(), keeping the message, the rate limiting and the signal.
> 
> Fixes: fc7157b84c32 ("trusted/ve/mmap: Protect from unsecure library load from CT image")
> Fixes: cc218b70c6b4 ("trusted/ve/fs/exec: Send SIGSEGV to a process trying to execute untrusted files")
> Feature: security/fs: tructed exec feature
> https://virtuozzo.atlassian.net/browse/VSTOR-137234
> Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
> ---
> v1 -> v2: commit message rewritten
> ---
>  kernel/ve/ve.c | 6 +++---
>  1 file changed, 3 insertions(+), 3 deletions(-)
> 
> diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
> index 73d1c3b4873e5..0d43dc3253ea3 100644
> --- a/kernel/ve/ve.c
> +++ b/kernel/ve/ve.c
> @@ -1745,8 +1745,8 @@ bool ve_check_trusted_mmap(struct file *file)
>  	if (file->f_path.dentry)
>  		filename = file->f_path.dentry->d_name.name;
>  
> -	WARN(1, "VE0 %s tried to map code from file '%s' from VEX\n",
> -			current->comm, filename);
> +	pr_warn("VE0 %s tried to map code from file '%s' from VEX\n",
> +		current->comm, filename);
>  	force_sigsegv(SIGSEGV);
>  	return false;
>  }
> @@ -1765,7 +1765,7 @@ bool ve_check_trusted_exec(struct file *file, struct filename *name)
>  	if (!__ratelimit(&sigsegv_rs))
>  		return false;
>  
> -	WARN(1, "VE0's %s tried to execute untrusted file %s from VEX\n",
> +	pr_warn("VE0's %s tried to execute untrusted file %s from VEX\n",
>  		current->comm, name->name);
>  	force_sigsegv(SIGSEGV);
>  	return false;

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

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH vz10 v2] selftests/uevent: do not fail on a netlink receive buffer overrun
  2026-08-24 13:09  3% ` [Devel] [PATCH vz10 v2] selftests/uevent: do not fail on a netlink receive buffer overrun Konstantin Khorenko
@ 2026-08-26 16:49  0%   ` Konstantin Khorenko
  2026-08-26 19:32  0%   ` Eva Kurchatova (Virtuozzo)
  2026-08-27 12:28  4%   ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
  2 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-26 16:49 UTC (permalink / raw)
  To: Vasileios Almpanis, Eva Kurchatova, OpenVZ devel list

a kind ping

--
Best regards,

Konstantin Khorenko,
Virtuozzo Linux Kernel Team

On 8/24/26 15:09, Konstantin Khorenko wrote:
> SO_RCVBUF is set to __UEVENT_BUFFER_SIZE, 4KB, which a busy machine
> overruns while the test is listening:
> 
>   No buffer space available - Failed to receive uevent
> 
> Two things are wrong here.
> 
> The socket queue is sized after a single message, while do_test()
> deliberately triggers ten uevents "to account for the case where the
> kernel might drop some", so the queue has to hold more than one.
> 
> Give it its own size and leave the message buffer alone: the kernel caps
> a single uevent at UEVENT_BUFFER_SIZE, 2048 bytes, so 4KB per message is
> already generous.
> 
> The receive loop then treats every error as fatal, ENOBUFS included,
> which defeats those ten uevents. Netlink clears the error after
> reporting it once, so the copies still queued, or still on their way,
> are perfectly receivable. Retry instead.
> 
> do_test() bounds the listener with a two second sigtimedwait(), so a
> retry cannot hang the test.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-139674
> Feature: fix selftests
> Reported-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
> Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
> ---
> Changes in v2:
> - keep __UEVENT_BUFFER_SIZE at 4KB and give SO_RCVBUF its own
>   __UEVENT_RCVBUF_SIZE.  v1 raised the shared macro, which sized the
>   socket queue correctly but also turned the per message buffer into a
>   128KB zero initialized array on the stack, while the kernel caps a
>   single uevent at UEVENT_BUFFER_SIZE, 2048 bytes.
> - retry recvmsg() on ENOBUFS instead of failing.  v1 only made the
>   overrun less likely; the test still died on the first one, even
>   though do_test() triggers ten uevents precisely so that drops are
>   tolerated.
> - subject and commit message updated accordingly.
> 
>  tools/testing/selftests/uevent/uevent_filtering.c | 13 ++++++++++++-
>  1 file changed, 12 insertions(+), 1 deletion(-)
> 
> diff --git a/tools/testing/selftests/uevent/uevent_filtering.c b/tools/testing/selftests/uevent/uevent_filtering.c
> index 8062804ff759..735eb8138c44 100644
> --- a/tools/testing/selftests/uevent/uevent_filtering.c
> +++ b/tools/testing/selftests/uevent/uevent_filtering.c
> @@ -23,6 +23,11 @@
>  
>  #define __DEV_FULL "/sys/devices/virtual/mem/full/uevent"
>  #define __UEVENT_BUFFER_SIZE (2048 * 2)
> +/*
> + * The socket queue has to hold more than a single message: the test
> + * triggers ten uevents and a busy machine overruns a small buffer.
> + */
> +#define __UEVENT_RCVBUF_SIZE (2048 * 64)
>  #define __UEVENT_HEADER "add@/devices/virtual/mem/full"
>  #define __UEVENT_HEADER_LEN sizeof("add@/devices/virtual/mem/full")
>  #define __UEVENT_LISTEN_ALL -1
> @@ -78,7 +83,7 @@ static int uevent_listener(unsigned long post_flags, bool expect_uevent,
>  {
>  	int sk_fd, ret;
>  	socklen_t sk_addr_len;
> -	int rcv_buf_sz = __UEVENT_BUFFER_SIZE;
> +	int rcv_buf_sz = __UEVENT_RCVBUF_SIZE;
>  	uint64_t sync_add = 1;
>  	struct sockaddr_nl sk_addr = { 0 }, rcv_addr = { 0 };
>  	char buf[__UEVENT_BUFFER_SIZE] = { 0 };
> @@ -158,6 +163,12 @@ static int uevent_listener(unsigned long post_flags, bool expect_uevent,
>  		ssize_t r;
>  
>  		r = recvmsg(sk_fd, &hdr, 0);
> +		/*
> +		 * The queue overran.  The kernel clears the error after
> +		 * reporting it once and more uevents are on their way.
> +		 */
> +		if (r < 0 && errno == ENOBUFS)
> +			continue;
>  		if (r <= 0) {
>  			fprintf(stderr, "%s - Failed to receive uevent\n", strerror(errno));
>  			ret = -1;

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

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH vz10 v2] selftests/uevent: do not fail on a netlink receive buffer overrun
  2026-08-24 13:09  3% ` [Devel] [PATCH vz10 v2] selftests/uevent: do not fail on a netlink receive buffer overrun Konstantin Khorenko
  2026-08-26 16:49  0%   ` Konstantin Khorenko
@ 2026-08-26 19:32  0%   ` Eva Kurchatova (Virtuozzo)
  2026-08-27 12:28  4%   ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
  2 siblings, 0 replies; 200+ results
From: Eva Kurchatova (Virtuozzo) @ 2026-08-26 19:32 UTC (permalink / raw)
  To: Konstantin Khorenko, Vasileios Almpanis, OpenVZ devel list


On 8/24/26 16:09, Konstantin Khorenko wrote:
> SO_RCVBUF is set to __UEVENT_BUFFER_SIZE, 4KB, which a busy machine
> overruns while the test is listening:
>
>    No buffer space available - Failed to receive uevent
>
> Two things are wrong here.
>
> The socket queue is sized after a single message, while do_test()
> deliberately triggers ten uevents "to account for the case where the
> kernel might drop some", so the queue has to hold more than one.
>
> Give it its own size and leave the message buffer alone: the kernel caps
> a single uevent at UEVENT_BUFFER_SIZE, 2048 bytes, so 4KB per message is
> already generous.
>
> The receive loop then treats every error as fatal, ENOBUFS included,
> which defeats those ten uevents. Netlink clears the error after
> reporting it once, so the copies still queued, or still on their way,
> are perfectly receivable. Retry instead.
>
> do_test() bounds the listener with a two second sigtimedwait(), so a
> retry cannot hang the test.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-139674
> Feature: fix selftests
> Reported-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
> Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
> ---
> Changes in v2:
> - keep __UEVENT_BUFFER_SIZE at 4KB and give SO_RCVBUF its own
>    __UEVENT_RCVBUF_SIZE.  v1 raised the shared macro, which sized the
>    socket queue correctly but also turned the per message buffer into a
>    128KB zero initialized array on the stack, while the kernel caps a
>    single uevent at UEVENT_BUFFER_SIZE, 2048 bytes.
> - retry recvmsg() on ENOBUFS instead of failing.  v1 only made the
>    overrun less likely; the test still died on the first one, even
>    though do_test() triggers ten uevents precisely so that drops are
>    tolerated.
> - subject and commit message updated accordingly.
>
>   tools/testing/selftests/uevent/uevent_filtering.c | 13 ++++++++++++-
>   1 file changed, 12 insertions(+), 1 deletion(-)
>
> diff --git a/tools/testing/selftests/uevent/uevent_filtering.c b/tools/testing/selftests/uevent/uevent_filtering.c
> index 8062804ff759..735eb8138c44 100644
> --- a/tools/testing/selftests/uevent/uevent_filtering.c
> +++ b/tools/testing/selftests/uevent/uevent_filtering.c
> @@ -23,6 +23,11 @@
>   
>   #define __DEV_FULL "/sys/devices/virtual/mem/full/uevent"
>   #define __UEVENT_BUFFER_SIZE (2048 * 2)
> +/*
> + * The socket queue has to hold more than a single message: the test
> + * triggers ten uevents and a busy machine overruns a small buffer.
> + */
> +#define __UEVENT_RCVBUF_SIZE (2048 * 64)
>   #define __UEVENT_HEADER "add@/devices/virtual/mem/full"
>   #define __UEVENT_HEADER_LEN sizeof("add@/devices/virtual/mem/full")
>   #define __UEVENT_LISTEN_ALL -1
> @@ -78,7 +83,7 @@ static int uevent_listener(unsigned long post_flags, bool expect_uevent,
>   {
>   	int sk_fd, ret;
>   	socklen_t sk_addr_len;
> -	int rcv_buf_sz = __UEVENT_BUFFER_SIZE;
> +	int rcv_buf_sz = __UEVENT_RCVBUF_SIZE;
>   	uint64_t sync_add = 1;
>   	struct sockaddr_nl sk_addr = { 0 }, rcv_addr = { 0 };
>   	char buf[__UEVENT_BUFFER_SIZE] = { 0 };
> @@ -158,6 +163,12 @@ static int uevent_listener(unsigned long post_flags, bool expect_uevent,
>   		ssize_t r;
>   
>   		r = recvmsg(sk_fd, &hdr, 0);
> +		/*
> +		 * The queue overran.  The kernel clears the error after
> +		 * reporting it once and more uevents are on their way.
> +		 */
> +		if (r < 0 && errno == ENOBUFS)
> +			continue;
>   		if (r <= 0) {
>   			fprintf(stderr, "%s - Failed to receive uevent\n", strerror(errno));
>   			ret = -1;

I was originally concerned about spinning on recvmsg(), but considering 
the socket is blocking, and recvmsg() should consume the error once, 
this should be fine.

Otherwise, this is a good improvement over the v1.


Acknowledged-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH RHEL10 COMMIT] selftests/uevent: do not fail on a netlink receive buffer overrun
  2026-08-24 13:09  3% ` [Devel] [PATCH vz10 v2] selftests/uevent: do not fail on a netlink receive buffer overrun Konstantin Khorenko
  2026-08-26 16:49  0%   ` Konstantin Khorenko
  2026-08-26 19:32  0%   ` Eva Kurchatova (Virtuozzo)
@ 2026-08-27 12:28  4%   ` Konstantin Khorenko
  2 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-27 12:28 UTC (permalink / raw)
  To: Konstantin Khorenko; +Cc: OpenVZ devel

The commit is pushed to "branch-rh10-6.12.0-211.39.1.16.x.vz10-ovz" and will appear at git@bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.39.1.16.11.vz10
------>
commit dd27dfa9341d66afa10abd7e082b2c32dfd160d0
Author: Konstantin Khorenko <khorenko@virtuozzo.com>
Date:   Mon Aug 24 15:09:22 2026 +0200

    selftests/uevent: do not fail on a netlink receive buffer overrun
    
    SO_RCVBUF is set to __UEVENT_BUFFER_SIZE, 4KB, which a busy machine
    overruns while the test is listening:
    
      No buffer space available - Failed to receive uevent
    
    Two things are wrong here.
    
    The socket queue is sized after a single message, while do_test()
    deliberately triggers ten uevents "to account for the case where the
    kernel might drop some", so the queue has to hold more than one.
    
    Give it its own size and leave the message buffer alone: the kernel caps
    a single uevent at UEVENT_BUFFER_SIZE, 2048 bytes, so 4KB per message is
    already generous.
    
    The receive loop then treats every error as fatal, ENOBUFS included,
    which defeats those ten uevents. Netlink clears the error after
    reporting it once, so the copies still queued, or still on their way,
    are perfectly receivable. Retry instead.
    
    do_test() bounds the listener with a two second sigtimedwait(), so a
    retry cannot hang the test.
    
    https://virtuozzo.atlassian.net/browse/VSTOR-139674
    Feature: fix selftests
    Reported-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
    Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
    Reviewed-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/uevent/uevent_filtering.c | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/tools/testing/selftests/uevent/uevent_filtering.c b/tools/testing/selftests/uevent/uevent_filtering.c
index 8062804ff759a..735eb8138c448 100644
--- a/tools/testing/selftests/uevent/uevent_filtering.c
+++ b/tools/testing/selftests/uevent/uevent_filtering.c
@@ -23,6 +23,11 @@
 
 #define __DEV_FULL "/sys/devices/virtual/mem/full/uevent"
 #define __UEVENT_BUFFER_SIZE (2048 * 2)
+/*
+ * The socket queue has to hold more than a single message: the test
+ * triggers ten uevents and a busy machine overruns a small buffer.
+ */
+#define __UEVENT_RCVBUF_SIZE (2048 * 64)
 #define __UEVENT_HEADER "add@/devices/virtual/mem/full"
 #define __UEVENT_HEADER_LEN sizeof("add@/devices/virtual/mem/full")
 #define __UEVENT_LISTEN_ALL -1
@@ -78,7 +83,7 @@ static int uevent_listener(unsigned long post_flags, bool expect_uevent,
 {
 	int sk_fd, ret;
 	socklen_t sk_addr_len;
-	int rcv_buf_sz = __UEVENT_BUFFER_SIZE;
+	int rcv_buf_sz = __UEVENT_RCVBUF_SIZE;
 	uint64_t sync_add = 1;
 	struct sockaddr_nl sk_addr = { 0 }, rcv_addr = { 0 };
 	char buf[__UEVENT_BUFFER_SIZE] = { 0 };
@@ -158,6 +163,12 @@ static int uevent_listener(unsigned long post_flags, bool expect_uevent,
 		ssize_t r;
 
 		r = recvmsg(sk_fd, &hdr, 0);
+		/*
+		 * The queue overran.  The kernel clears the error after
+		 * reporting it once and more uevents are on their way.
+		 */
+		if (r < 0 && errno == ENOBUFS)
+			continue;
 		if (r <= 0) {
 			fprintf(stderr, "%s - Failed to receive uevent\n", strerror(errno));
 			ret = -1;
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* [Devel] [PATCH VZ10] fs/fuse kio: track pending kRPC connect via state machine only
@ 2026-08-27 12:37  3% Liu Kui
  2026-08-28 17:13  0% ` Konstantin Khorenko
  2026-09-01 20:59  0% ` Konstantin Khorenko
  0 siblings, 2 replies; 200+ results
From: Liu Kui @ 2026-08-27 12:37 UTC (permalink / raw)
  To: devel; +Cc: azaitsev, Liu Kui

Rework the previous fix ("fs/fuse kio: fix kRPC connect issues") to not
require the new struct pcs_krpc member "connect_req", so the fix can be
shipped as a livepatch.

Both things connect_req was tracking are already derivable from the
existing state machine once PCS_KRPC_STATE_CONNECT is made to mean
exactly "a connect req is in flight":

 - krpc_connect_done() settles a failed connect back to UNCONN instead
   of leaving the state in CONNECT forever;

 - pcs_krpc_abort() no longer resets CONNECT to UNCONN: the req is
   still in flight, and only its completion settles the state;

 - pcs_krpc_connect() proceeds only from UNCONN or ABORTED, refusing
   new connects (-EPERM) while a req is in flight - at most one connect
   req exists at a time, same as with the connect_req check;

 - pcs_krpc_poll() reports EPOLLERR on UNCONN: poll bails out earlier
   unless ctx->gen == krpc->gen, and the current session can only be in
   UNCONN if its connect failed or was aborted, which is what the
   (CONNECT && !connect_req) test used to detect.

gen only advances in pcs_krpc_connect(), which is blocked during
CONNECT, so within that state the in-flight req always carries the
current gen and krpc_connect_done()'s existing staleness check is
sufficient.

Related to:
https://virtuozzo.atlassian.net/browse/VSTOR-135626

Signed-off-by: Liu Kui <kui.liu@virtuozzo.com>
---
 fs/fuse/kio/pcs/pcs_krpc.c | 39 ++++++++++++++++++++++++++++----------
 fs/fuse/kio/pcs/pcs_krpc.h |  2 --
 2 files changed, 29 insertions(+), 12 deletions(-)

diff --git a/fs/fuse/kio/pcs/pcs_krpc.c b/fs/fuse/kio/pcs/pcs_krpc.c
index b55c093c13d0..bcfbfc6d9304 100644
--- a/fs/fuse/kio/pcs/pcs_krpc.c
+++ b/fs/fuse/kio/pcs/pcs_krpc.c
@@ -738,8 +738,12 @@ static int pcs_krpc_abort(struct pcs_krpc *krpc)
 	spin_lock(&krpc->lock);
 
 	if (krpc->state != PCS_KRPC_STATE_CONNECTED) {
-		if (krpc->state == PCS_KRPC_STATE_CONNECT)
-			krpc->state = PCS_KRPC_STATE_UNCONN;
+		/*
+		 * A pending connect stays in CONNECT state: its connect req
+		 * is still in flight and krpc_connect_done() will settle the
+		 * state to UNCONN when it completes.  Until then new connects
+		 * are refused, so at most one connect req exists at a time.
+		 */
 		spin_unlock(&krpc->lock);
 		return 0;
 	}
@@ -907,8 +911,13 @@ static __poll_t pcs_krpc_poll(struct file *file, poll_table *wait)
 
 	spin_lock(&krpc->lock);
 
+	/*
+	 * ctx->gen == krpc->gen (checked above) means this is the current
+	 * session, so UNCONN here can only mean its connect attempt has
+	 * failed (see krpc_connect_done()) or the session was aborted.
+	 */
 	if (krpc->state == PCS_KRPC_STATE_ABORTED ||
-	    (krpc->state == PCS_KRPC_STATE_CONNECT && !krpc->connect_req)) {
+	    krpc->state == PCS_KRPC_STATE_UNCONN) {
 		pollflags |= EPOLLERR;
 	} else if (krpc->state == PCS_KRPC_STATE_CONNECTED) {
 		pollflags |= EPOLLOUT;
@@ -999,7 +1008,6 @@ int pcs_krpc_create(struct pcs_krpc_set *krpcs, PCS_NODE_ID_T *id,
 	krpc->gen = 0;
 	krpc->state = PCS_KRPC_STATE_UNCONN;
 	krpc->cs = NULL;
-	krpc->connect_req = NULL;
 
 	krpc->rpc = pcs_rpc_clnt_create(&cc_from_krpcset(krpcs)->eng, id, addr, cs_flags);
 	if (!krpc->rpc) {
@@ -1051,8 +1059,6 @@ static void krpc_connect_done(struct pcs_msg *msg)
 	}
 
 	spin_lock(&krpc->lock);
-	if (krpc->connect_req == req)
-		krpc->connect_req = NULL;
 	/* from a stale session, do nothing  */
 	if (req->gen != krpc->gen || krpc->state != PCS_KRPC_STATE_CONNECT) {
 		spin_unlock(&krpc->lock);
@@ -1062,6 +1068,14 @@ static void krpc_connect_done(struct pcs_msg *msg)
 	if (!pcs_if_error(&msg->error)) {
 		krpc->state = PCS_KRPC_STATE_CONNECTED;
 		pollflags = EPOLLOUT;
+	} else {
+		/*
+		 * Connect failed: settle back to UNCONN so that a new connect
+		 * is allowed again, and report the failure to poll().  Since
+		 * gen is unchanged, the current session's poll sees UNCONN
+		 * and returns EPOLLERR.
+		 */
+		krpc->state = PCS_KRPC_STATE_UNCONN;
 	}
 	spin_unlock(&krpc->lock);
 
@@ -1119,9 +1133,15 @@ int pcs_krpc_connect(struct pcs_krpc_set *krpcs, PCS_NODE_ID_T *id)
 	}
 
 	spin_lock(&krpc->lock);
-	if (krpc->state == PCS_KRPC_STATE_CONNECTED ||
-	    krpc->state == PCS_KRPC_STATE_DESTROYED ||
-	    krpc->connect_req) {
+	/*
+	 * A connect is allowed only when there is neither an established
+	 * session nor a connect req in flight (CONNECT state, see
+	 * krpc_connect_done()).  This limits connect reqs to one at a time:
+	 * if userspace gave up on a connect and retries, the new connect
+	 * fails immediately until the old req completes.
+	 */
+	if (krpc->state != PCS_KRPC_STATE_UNCONN &&
+	    krpc->state != PCS_KRPC_STATE_ABORTED) {
 		spin_unlock(&krpc->lock);
 		err = -EPERM;
 		/* fput() drops ctx and its krpc reference via pcs_krpc_release() */
@@ -1133,7 +1153,6 @@ int pcs_krpc_connect(struct pcs_krpc_set *krpcs, PCS_NODE_ID_T *id)
 	connect_req->gen = krpc->gen;
 	connect_req->krpc = pcs_krpc_get(krpc);
 	krpc->state = PCS_KRPC_STATE_CONNECT;
-	krpc->connect_req = connect_req;
 	spin_unlock(&krpc->lock);
 
 	/* publish the fd only after the connect is committed */
diff --git a/fs/fuse/kio/pcs/pcs_krpc.h b/fs/fuse/kio/pcs/pcs_krpc.h
index 3db9e9712019..6a090ef66185 100644
--- a/fs/fuse/kio/pcs/pcs_krpc.h
+++ b/fs/fuse/kio/pcs/pcs_krpc.h
@@ -82,8 +82,6 @@ struct pcs_krpc {
 	/** Wait queue head for poll */
 	wait_queue_head_t		poll_wait;
 	struct pcs_cs			*cs;
-
-	struct krpc_connect_req *connect_req;
 };
 
 struct pcs_krpc_context {
-- 
2.50.1 (Apple Git-155)

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

^ permalink raw reply	[relevance 3%]

* [PATCH QEMU HCI-8.0] b4: ship project defaults in .b4-config
@ 2026-08-28 11:15 12% Vasileios Almpanis
  0 siblings, 0 replies; 200+ results
From: Vasileios Almpanis @ 2026-08-28 11:15 UTC (permalink / raw)
  To: Andrey Drobyshev; +Cc: svt-core, Vasileios Almpanis

b4 fetches series from lore.kernel.org by default. To use it with our
list archive at lore.virtuozzo.com every developer had to set the same
handful of b4.* git config keys, plus two more to get the right
recipients on "b4 send" and one for the subject prefix.

b4 loads a .b4-config at the top of the tree as defaults for commands
run inside it, so put the archive URL masks, the default recipients and
the default subject prefix there.

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

Signed-off-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
---
 .b4-config | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/.b4-config b/.b4-config
index 126f503ded7..c8afd7b3e61 100644
--- a/.b4-config
+++ b/.b4-config
@@ -4,10 +4,13 @@
 #
 
 [b4]
-    send-series-to = qemu-devel@nongnu.org
+    send-series-to = "Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>"
+    send-series-cc = svt-core@virtuozzo.com
+    send-prefixes = "QEMU HCI-8.0"
     send-auto-to-cmd = echo
     send-auto-cc-cmd = scripts/get_maintainer.pl --noroles --norolestats --nogit --nogit-fallback
     am-perpatch-check-cmd = scripts/checkpatch.pl -q --terse --no-summary --mailback -
     prep-perpatch-check-cmd = scripts/checkpatch.pl -q --terse --no-summary --mailback -
-    searchmask = https://lore.kernel.org/qemu-devel/?x=m&t=1&q=%s
-    linkmask = https://lore.kernel.org/qemu-devel/%s
+    midmask = http://lore.virtuozzo.com/%s
+    searchmask = http://lore.virtuozzo.com/all/?x=m&t=1&q=%s
+    linkmask = http://lore.virtuozzo.com/%s

---
base-commit: c8606682c5168c07badb55aebaa55a05db1d5376
change-id: 20260828-b4-a3aaa05eb6aa

-- 
Best regards, Vasileios Almpanis
Software Developer, Virtuozzo.


^ permalink raw reply	[relevance 12%]

* [PATCH QEMU HCI-8.0] b4: ship project defaults in .b4-config
@ 2026-08-28 11:16 12% Vasileios Almpanis
  2026-08-28 11:40  4% ` Andrey Drobyshev
  0 siblings, 1 reply; 200+ results
From: Vasileios Almpanis @ 2026-08-28 11:16 UTC (permalink / raw)
  To: Andrey Drobyshev; +Cc: svt-core, Vasileios Almpanis

b4 fetches series from lore.kernel.org by default. To use it with our
list archive at lore.virtuozzo.com every developer had to set the same
handful of b4.* git config keys, plus two more to get the right
recipients on "b4 send" and one for the subject prefix.

b4 loads a .b4-config at the top of the tree as defaults for commands
run inside it, so put the archive URL masks, the default recipients and
the default subject prefix there.

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

Signed-off-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
---
 .b4-config | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/.b4-config b/.b4-config
index 126f503ded7..c8afd7b3e61 100644
--- a/.b4-config
+++ b/.b4-config
@@ -4,10 +4,13 @@
 #
 
 [b4]
-    send-series-to = qemu-devel@nongnu.org
+    send-series-to = "Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>"
+    send-series-cc = svt-core@virtuozzo.com
+    send-prefixes = "QEMU HCI-8.0"
     send-auto-to-cmd = echo
     send-auto-cc-cmd = scripts/get_maintainer.pl --noroles --norolestats --nogit --nogit-fallback
     am-perpatch-check-cmd = scripts/checkpatch.pl -q --terse --no-summary --mailback -
     prep-perpatch-check-cmd = scripts/checkpatch.pl -q --terse --no-summary --mailback -
-    searchmask = https://lore.kernel.org/qemu-devel/?x=m&t=1&q=%s
-    linkmask = https://lore.kernel.org/qemu-devel/%s
+    midmask = http://lore.virtuozzo.com/%s
+    searchmask = http://lore.virtuozzo.com/all/?x=m&t=1&q=%s
+    linkmask = http://lore.virtuozzo.com/%s

---
base-commit: c8606682c5168c07badb55aebaa55a05db1d5376
change-id: 20260828-b4-a3aaa05eb6aa

-- 
Best regards, Vasileios Almpanis
Software Developer, Virtuozzo.


^ permalink raw reply	[relevance 12%]

* Re: [PATCH QEMU HCI-8.0] b4: ship project defaults in .b4-config
  2026-08-28 11:16 12% Vasileios Almpanis
@ 2026-08-28 11:40  4% ` Andrey Drobyshev
  0 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-08-28 11:40 UTC (permalink / raw)
  To: Vasileios Almpanis; +Cc: svt-core

On 8/28/26 2:16 PM, Vasileios Almpanis wrote:
> b4 fetches series from lore.kernel.org by default. To use it with our
> list archive at lore.virtuozzo.com every developer had to set the same
> handful of b4.* git config keys, plus two more to get the right
> recipients on "b4 send" and one for the subject prefix.
> 
> b4 loads a .b4-config at the top of the tree as defaults for commands
> run inside it, so put the archive URL masks, the default recipients and
> the default subject prefix there.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-142960
> 
> Signed-off-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
> ---
>  .b4-config | 9 ++++++---
>  1 file changed, 6 insertions(+), 3 deletions(-)
> 
> diff --git a/.b4-config b/.b4-config
> index 126f503ded7..c8afd7b3e61 100644
> --- a/.b4-config
> +++ b/.b4-config
> @@ -4,10 +4,13 @@
>  #
>  
>  [b4]
> -    send-series-to = qemu-devel@nongnu.org
> +    send-series-to = "Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>"
> +    send-series-cc = svt-core@virtuozzo.com

I like the default better, i.e. --to is the list, --cc is the
maintainer(s).  I'll adjust it myself.

> +    send-prefixes = "QEMU HCI-8.0"
>      send-auto-to-cmd = echo
>      send-auto-cc-cmd = scripts/get_maintainer.pl --noroles --norolestats --nogit --nogit-fallback
>      am-perpatch-check-cmd = scripts/checkpatch.pl -q --terse --no-summary --mailback -
>      prep-perpatch-check-cmd = scripts/checkpatch.pl -q --terse --no-summary --mailback -
> -    searchmask = https://lore.kernel.org/qemu-devel/?x=m&t=1&q=%s
> -    linkmask = https://lore.kernel.org/qemu-devel/%s
> +    midmask = http://lore.virtuozzo.com/%s
> +    searchmask = http://lore.virtuozzo.com/all/?x=m&t=1&q=%s

I'm wondering isn't it better to explicitly specify /qemu here?  It'll
probably make the search faster.

Andrey

> +    linkmask = http://lore.virtuozzo.com/%s
> 
> ---
> base-commit: c8606682c5168c07badb55aebaa55a05db1d5376
> change-id: 20260828-b4-a3aaa05eb6aa
> 


^ permalink raw reply	[relevance 4%]

* Re: [Devel] [PATCH VZ10 v7 7/9] ve: Introduce per-VE failcount
  2026-08-24 13:54  7% ` [Devel] [PATCH VZ10 v7 7/9] ve: Introduce per-VE failcount Vladimir Riabchun
@ 2026-08-28 16:52  4%   ` Pavel Tikhomirov
  2026-08-28 17:32  0%     ` Vladimir Riabchun
  0 siblings, 1 reply; 200+ results
From: Pavel Tikhomirov @ 2026-08-28 16:52 UTC (permalink / raw)
  To: Vladimir Riabchun, devel



On 8/24/26 15:54, 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       |  6 ++++
>  kernel/bpf/syscall.c     |  1 +
>  kernel/ve/ve.c           | 70 ++++++++++++++++++++++++++++++++++++++++
>  net/core/dev.c           |  2 ++
>  net/core/neighbour.c     |  1 +
>  net/core/net_namespace.c |  4 ++-
>  8 files changed, 86 insertions(+), 1 deletion(-)
> 
> diff --git a/fs/aio.c b/fs/aio.c
> index cb63416af135..3fa07cc626f8 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) {
> +		atomic_inc(&ve->aio_failcount);
>  		spin_unlock(&ve->aio_nr_lock);
>  		err = -EAGAIN;
>  		goto err_ctx;
> diff --git a/fs/namespace.c b/fs/namespace.c
> index e97f48204617..c30bbc370f2b 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
> @@ -3371,6 +3371,8 @@ static inline int ve_try_reserve_mount(struct ve_struct *ve)
>  
>  	if (ret)
>  		get_ve(ve);
> +	else
> +		atomic_inc(&ve->mnt_failcount);
>  	return ret;
>  }
>  
> diff --git a/include/linux/ve.h b/include/linux/ve.h
> index 5687faad46ff..9e73527e970e 100644
> --- a/include/linux/ve.h
> +++ b/include/linux/ve.h
> @@ -72,12 +72,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;
> @@ -86,6 +89,7 @@ struct ve_struct {
>  
>  	atomic_t		arp_neigh_nr;
>  	atomic_t		nd_neigh_nr;
> +	atomic_t		neigh_tbl_failcount;
>  	unsigned long		meminfo_val;
>  
>  	/*
> @@ -94,6 +98,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
> @@ -121,6 +126,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;
>  };
> diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
> index c94d4240e3d3..9d57e7999ae0 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) {
> +			atomic_inc(&load_ve->bpf_prog_failcount);
>  			load_ve = NULL;
>  			err = -ENOSPC;
>  			goto put_token;
> diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
> index 0f02835765ff..826f72ad0a22 100644
> --- a/kernel/ve/ve.c
> +++ b/kernel/ve/ve.c
> @@ -99,10 +99,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	=
> @@ -114,8 +117,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,

This feature is not intended to limit host aio, right? Should it
be INT_MAX or something big here for ve0?

> +	.aio_failcount		= ATOMIC_INIT(0),
> +#endif
>  	.meminfo_val		= VE_MEMINFO_SYSTEM,
>  	.umh_running_helpers	= ATOMIC_INIT(0),
>  	.umh_helpers_waitq	= __WAIT_QUEUE_HEAD_INITIALIZER(ve0.umh_helpers_waitq),
> @@ -780,12 +791,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);
> @@ -812,7 +826,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
> @@ -825,6 +841,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;
> @@ -1065,6 +1082,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);
> @@ -1602,6 +1666,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..05e0b9b6ba23 100644
> --- a/net/core/dev.c
> +++ b/net/core/dev.c
> @@ -10997,6 +10997,7 @@ int register_netdevice(struct net_device *dev)
>  
>  	ret = -ENOMEM;
>  	if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
> +		atomic_inc(&net->owner_ve->netif_failcount);
>  		ve_pr_warn_ratelimited(VE_LOG_BOTH,
>  			"CT%s: hits max number of network devices, "
>  			"increase ve::netif_max_nr parameter\n",
> @@ -12211,6 +12212,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) {
> +		atomic_inc(&net->owner_ve->netif_failcount);
>  		ve_pr_warn_ratelimited(VE_LOG_BOTH,
>  			"CT%s: hits max number of network devices, "
>  			"increase ve::netif_max_nr parameter\n",
> diff --git a/net/core/neighbour.c b/net/core/neighbour.c
> index f90deb17fb25..57a49d9c98a7 100644
> --- a/net/core/neighbour.c
> +++ b/net/core/neighbour.c
> @@ -520,6 +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) {
> +			atomic_inc(&ve->neigh_tbl_failcount);
>  			net_info_ratelimited("%s: neighbor table overflow!\n",
>  					     tbl->id);
>  			NEIGH_CACHE_STAT_INC(tbl, table_fulls);
> diff --git a/net/core/net_namespace.c b/net/core/net_namespace.c
> index b3d54cad984a..9a3376d2682f 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) {
> +		atomic_inc(&ve->netns_failcount);

Let's add a helper for incrementing our failcounts:

#define ve_failcount_inc(ve, name)                                    \
do {                                                                  \
      struct ve_struct *__ve = (ve);                                  \
                                                                      \
      if (atomic_inc_return(&__ve->name##_failcount) == 1)            \
              pr_warn("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)

Call sites become one-liners:

--- a/fs/aio.c
-             atomic_inc(&ve->aio_failcount);
+             ve_failcount_inc(ve, aio);
--- a/fs/namespace.c
-             atomic_inc(&ve->mnt_failcount);
+             ve_failcount_inc(ve, mnt);
--- a/kernel/bpf/syscall.c
-                     atomic_inc(&load_ve->bpf_prog_failcount);
+                     ve_failcount_inc(load_ve, bpf_prog);
--- a/net/core/dev.c          (both register_netdevice() and __dev_change_net_namespace())
-             atomic_inc(&net->owner_ve->netif_failcount);
+             ve_failcount_inc(net->owner_ve, netif);
--- a/net/core/neighbour.c
-                     atomic_inc(&ve->neigh_tbl_failcount);
+                     ve_failcount_inc(ve, neigh_tbl);
--- a/net/core/net_namespace.c
-             atomic_inc(&ve->netns_failcount);
+             ve_failcount_inc(ve, netns);

The idea behind it is to show in dmesg that failcount was reached, to simpify
the detection of problematic containers for us.

For reference http://lore.virtuozzo.com/kernel/ccc3cbc8-e225-41cf-b563-699254aeb57b@virtuozzo.com/T/#t

>  		return -ENOSPC;
> +	}
>  	return 0;
>  }
>  

-- 
Best regards, Pavel Tikhomirov
Senior Software Developer, Virtuozzo.

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

^ permalink raw reply	[relevance 4%]

* Re: [Devel] [PATCH VZ10] fs/fuse kio: track pending kRPC connect via state machine only
  2026-08-27 12:37  3% [Devel] [PATCH VZ10] fs/fuse kio: track pending kRPC connect via state machine only Liu Kui
@ 2026-08-28 17:13  0% ` Konstantin Khorenko
  2026-09-01 20:59  0% ` Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-08-28 17:13 UTC (permalink / raw)
  To: Liu Kui, devel; +Cc: azaitsev

Liu, please explain what do i do with that patch?
Should i revert ("fs/fuse kio: fix kRPC connect issues") and push this patch instead?

Should we release it as an RK? If yes - for which kernels and where are the corresponding bugs?
Currently i understand nothing, sorry.

--
Best regards,

Konstantin Khorenko,
Virtuozzo Linux Kernel Team

On 8/27/26 14:37, Liu Kui wrote:
> Rework the previous fix ("fs/fuse kio: fix kRPC connect issues") to not
> require the new struct pcs_krpc member "connect_req", so the fix can be
> shipped as a livepatch.
> 
> Both things connect_req was tracking are already derivable from the
> existing state machine once PCS_KRPC_STATE_CONNECT is made to mean
> exactly "a connect req is in flight":
> 
>  - krpc_connect_done() settles a failed connect back to UNCONN instead
>    of leaving the state in CONNECT forever;
> 
>  - pcs_krpc_abort() no longer resets CONNECT to UNCONN: the req is
>    still in flight, and only its completion settles the state;
> 
>  - pcs_krpc_connect() proceeds only from UNCONN or ABORTED, refusing
>    new connects (-EPERM) while a req is in flight - at most one connect
>    req exists at a time, same as with the connect_req check;
> 
>  - pcs_krpc_poll() reports EPOLLERR on UNCONN: poll bails out earlier
>    unless ctx->gen == krpc->gen, and the current session can only be in
>    UNCONN if its connect failed or was aborted, which is what the
>    (CONNECT && !connect_req) test used to detect.
> 
> gen only advances in pcs_krpc_connect(), which is blocked during
> CONNECT, so within that state the in-flight req always carries the
> current gen and krpc_connect_done()'s existing staleness check is
> sufficient.
> 
> Related to:
> https://virtuozzo.atlassian.net/browse/VSTOR-135626
> 
> Signed-off-by: Liu Kui <kui.liu@virtuozzo.com>
> ---
>  fs/fuse/kio/pcs/pcs_krpc.c | 39 ++++++++++++++++++++++++++++----------
>  fs/fuse/kio/pcs/pcs_krpc.h |  2 --
>  2 files changed, 29 insertions(+), 12 deletions(-)
> 
> diff --git a/fs/fuse/kio/pcs/pcs_krpc.c b/fs/fuse/kio/pcs/pcs_krpc.c
> index b55c093c13d0..bcfbfc6d9304 100644
> --- a/fs/fuse/kio/pcs/pcs_krpc.c
> +++ b/fs/fuse/kio/pcs/pcs_krpc.c
> @@ -738,8 +738,12 @@ static int pcs_krpc_abort(struct pcs_krpc *krpc)
>  	spin_lock(&krpc->lock);
>  
>  	if (krpc->state != PCS_KRPC_STATE_CONNECTED) {
> -		if (krpc->state == PCS_KRPC_STATE_CONNECT)
> -			krpc->state = PCS_KRPC_STATE_UNCONN;
> +		/*
> +		 * A pending connect stays in CONNECT state: its connect req
> +		 * is still in flight and krpc_connect_done() will settle the
> +		 * state to UNCONN when it completes.  Until then new connects
> +		 * are refused, so at most one connect req exists at a time.
> +		 */
>  		spin_unlock(&krpc->lock);
>  		return 0;
>  	}
> @@ -907,8 +911,13 @@ static __poll_t pcs_krpc_poll(struct file *file, poll_table *wait)
>  
>  	spin_lock(&krpc->lock);
>  
> +	/*
> +	 * ctx->gen == krpc->gen (checked above) means this is the current
> +	 * session, so UNCONN here can only mean its connect attempt has
> +	 * failed (see krpc_connect_done()) or the session was aborted.
> +	 */
>  	if (krpc->state == PCS_KRPC_STATE_ABORTED ||
> -	    (krpc->state == PCS_KRPC_STATE_CONNECT && !krpc->connect_req)) {
> +	    krpc->state == PCS_KRPC_STATE_UNCONN) {
>  		pollflags |= EPOLLERR;
>  	} else if (krpc->state == PCS_KRPC_STATE_CONNECTED) {
>  		pollflags |= EPOLLOUT;
> @@ -999,7 +1008,6 @@ int pcs_krpc_create(struct pcs_krpc_set *krpcs, PCS_NODE_ID_T *id,
>  	krpc->gen = 0;
>  	krpc->state = PCS_KRPC_STATE_UNCONN;
>  	krpc->cs = NULL;
> -	krpc->connect_req = NULL;
>  
>  	krpc->rpc = pcs_rpc_clnt_create(&cc_from_krpcset(krpcs)->eng, id, addr, cs_flags);
>  	if (!krpc->rpc) {
> @@ -1051,8 +1059,6 @@ static void krpc_connect_done(struct pcs_msg *msg)
>  	}
>  
>  	spin_lock(&krpc->lock);
> -	if (krpc->connect_req == req)
> -		krpc->connect_req = NULL;
>  	/* from a stale session, do nothing  */
>  	if (req->gen != krpc->gen || krpc->state != PCS_KRPC_STATE_CONNECT) {
>  		spin_unlock(&krpc->lock);
> @@ -1062,6 +1068,14 @@ static void krpc_connect_done(struct pcs_msg *msg)
>  	if (!pcs_if_error(&msg->error)) {
>  		krpc->state = PCS_KRPC_STATE_CONNECTED;
>  		pollflags = EPOLLOUT;
> +	} else {
> +		/*
> +		 * Connect failed: settle back to UNCONN so that a new connect
> +		 * is allowed again, and report the failure to poll().  Since
> +		 * gen is unchanged, the current session's poll sees UNCONN
> +		 * and returns EPOLLERR.
> +		 */
> +		krpc->state = PCS_KRPC_STATE_UNCONN;
>  	}
>  	spin_unlock(&krpc->lock);
>  
> @@ -1119,9 +1133,15 @@ int pcs_krpc_connect(struct pcs_krpc_set *krpcs, PCS_NODE_ID_T *id)
>  	}
>  
>  	spin_lock(&krpc->lock);
> -	if (krpc->state == PCS_KRPC_STATE_CONNECTED ||
> -	    krpc->state == PCS_KRPC_STATE_DESTROYED ||
> -	    krpc->connect_req) {
> +	/*
> +	 * A connect is allowed only when there is neither an established
> +	 * session nor a connect req in flight (CONNECT state, see
> +	 * krpc_connect_done()).  This limits connect reqs to one at a time:
> +	 * if userspace gave up on a connect and retries, the new connect
> +	 * fails immediately until the old req completes.
> +	 */
> +	if (krpc->state != PCS_KRPC_STATE_UNCONN &&
> +	    krpc->state != PCS_KRPC_STATE_ABORTED) {
>  		spin_unlock(&krpc->lock);
>  		err = -EPERM;
>  		/* fput() drops ctx and its krpc reference via pcs_krpc_release() */
> @@ -1133,7 +1153,6 @@ int pcs_krpc_connect(struct pcs_krpc_set *krpcs, PCS_NODE_ID_T *id)
>  	connect_req->gen = krpc->gen;
>  	connect_req->krpc = pcs_krpc_get(krpc);
>  	krpc->state = PCS_KRPC_STATE_CONNECT;
> -	krpc->connect_req = connect_req;
>  	spin_unlock(&krpc->lock);
>  
>  	/* publish the fd only after the connect is committed */
> diff --git a/fs/fuse/kio/pcs/pcs_krpc.h b/fs/fuse/kio/pcs/pcs_krpc.h
> index 3db9e9712019..6a090ef66185 100644
> --- a/fs/fuse/kio/pcs/pcs_krpc.h
> +++ b/fs/fuse/kio/pcs/pcs_krpc.h
> @@ -82,8 +82,6 @@ struct pcs_krpc {
>  	/** Wait queue head for poll */
>  	wait_queue_head_t		poll_wait;
>  	struct pcs_cs			*cs;
> -
> -	struct krpc_connect_req *connect_req;
>  };
>  
>  struct pcs_krpc_context {

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

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH VZ10 v7 7/9] ve: Introduce per-VE failcount
  2026-08-28 16:52  4%   ` Pavel Tikhomirov
@ 2026-08-28 17:32  0%     ` Vladimir Riabchun
  0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-08-28 17:32 UTC (permalink / raw)
  To: Pavel Tikhomirov, devel



On 8/28/26 18:52, Pavel Tikhomirov wrote:
> 
> 
> On 8/24/26 15:54, 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       |  6 ++++
>>   kernel/bpf/syscall.c     |  1 +
>>   kernel/ve/ve.c           | 70 ++++++++++++++++++++++++++++++++++++++++
>>   net/core/dev.c           |  2 ++
>>   net/core/neighbour.c     |  1 +
>>   net/core/net_namespace.c |  4 ++-
>>   8 files changed, 86 insertions(+), 1 deletion(-)
>>
>> diff --git a/fs/aio.c b/fs/aio.c
>> index cb63416af135..3fa07cc626f8 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) {
>> +		atomic_inc(&ve->aio_failcount);
>>   		spin_unlock(&ve->aio_nr_lock);
>>   		err = -EAGAIN;
>>   		goto err_ctx;
>> diff --git a/fs/namespace.c b/fs/namespace.c
>> index e97f48204617..c30bbc370f2b 100644
>> --- a/fs/namespace.c
>> +++ b/fs/namespace.c
>> @@ -3371,6 +3371,8 @@ static inline int ve_try_reserve_mount(struct ve_struct *ve)
>>   
>>   	if (ret)
>>   		get_ve(ve);
>> +	else
>> +		atomic_inc(&ve->mnt_failcount);
>>   	return ret;
>>   }
>>   
>> diff --git a/include/linux/ve.h b/include/linux/ve.h
>> index 5687faad46ff..9e73527e970e 100644
>> --- a/include/linux/ve.h
>> +++ b/include/linux/ve.h
>> @@ -72,12 +72,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;
>> @@ -86,6 +89,7 @@ struct ve_struct {
>>   
>>   	atomic_t		arp_neigh_nr;
>>   	atomic_t		nd_neigh_nr;
>> +	atomic_t		neigh_tbl_failcount;
>>   	unsigned long		meminfo_val;
>>   
>>   	/*
>> @@ -94,6 +98,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
>> @@ -121,6 +126,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;
>>   };
>> diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
>> index c94d4240e3d3..9d57e7999ae0 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) {
>> +			atomic_inc(&load_ve->bpf_prog_failcount);
>>   			load_ve = NULL;
>>   			err = -ENOSPC;
>>   			goto put_token;
>> diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
>> index 0f02835765ff..826f72ad0a22 100644
>> --- a/kernel/ve/ve.c
>> +++ b/kernel/ve/ve.c
>> @@ -99,10 +99,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	=
>> @@ -114,8 +117,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,
> 
> This feature is not intended to limit host aio, right? Should it
> be INT_MAX or something big here for ve0?

It preserves original kernel behavior. Upstream kernel has aio_max_nr
sysctl variable with the same value as AIO_MAX_NR_DEFAULT.
Commit 1f6006388601 ("ve/fs/aio: aio_nr & aio_max_nr variables virtualization")
added aio_max_nr to ve structure, but it didn't initialize it in ve0.

Here I just put the correct value in it, so no functional changes.

> 
>> +	.aio_failcount		= ATOMIC_INIT(0),
>> +#endif
>>   	.meminfo_val		= VE_MEMINFO_SYSTEM,
>>   	.umh_running_helpers	= ATOMIC_INIT(0),
>>   	.umh_helpers_waitq	= __WAIT_QUEUE_HEAD_INITIALIZER(ve0.umh_helpers_waitq),
>> @@ -780,12 +791,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);
>> @@ -812,7 +826,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
>> @@ -825,6 +841,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;
>> @@ -1065,6 +1082,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);
>> @@ -1602,6 +1666,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..05e0b9b6ba23 100644
>> --- a/net/core/dev.c
>> +++ b/net/core/dev.c
>> @@ -10997,6 +10997,7 @@ int register_netdevice(struct net_device *dev)
>>   
>>   	ret = -ENOMEM;
>>   	if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
>> +		atomic_inc(&net->owner_ve->netif_failcount);
>>   		ve_pr_warn_ratelimited(VE_LOG_BOTH,
>>   			"CT%s: hits max number of network devices, "
>>   			"increase ve::netif_max_nr parameter\n",
>> @@ -12211,6 +12212,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) {
>> +		atomic_inc(&net->owner_ve->netif_failcount);
>>   		ve_pr_warn_ratelimited(VE_LOG_BOTH,
>>   			"CT%s: hits max number of network devices, "
>>   			"increase ve::netif_max_nr parameter\n",
>> diff --git a/net/core/neighbour.c b/net/core/neighbour.c
>> index f90deb17fb25..57a49d9c98a7 100644
>> --- a/net/core/neighbour.c
>> +++ b/net/core/neighbour.c
>> @@ -520,6 +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) {
>> +			atomic_inc(&ve->neigh_tbl_failcount);
>>   			net_info_ratelimited("%s: neighbor table overflow!\n",
>>   					     tbl->id);
>>   			NEIGH_CACHE_STAT_INC(tbl, table_fulls);
>> diff --git a/net/core/net_namespace.c b/net/core/net_namespace.c
>> index b3d54cad984a..9a3376d2682f 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) {
>> +		atomic_inc(&ve->netns_failcount);
> 
> Let's add a helper for incrementing our failcounts:
> 
> #define ve_failcount_inc(ve, name)                                    \
> do {                                                                  \
>        struct ve_struct *__ve = (ve);                                  \
>                                                                        \
>        if (atomic_inc_return(&__ve->name##_failcount) == 1)            \
>                pr_warn("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)
> 
> Call sites become one-liners:
> 
> --- a/fs/aio.c
> -             atomic_inc(&ve->aio_failcount);
> +             ve_failcount_inc(ve, aio);
> --- a/fs/namespace.c
> -             atomic_inc(&ve->mnt_failcount);
> +             ve_failcount_inc(ve, mnt);
> --- a/kernel/bpf/syscall.c
> -                     atomic_inc(&load_ve->bpf_prog_failcount);
> +                     ve_failcount_inc(load_ve, bpf_prog);
> --- a/net/core/dev.c          (both register_netdevice() and __dev_change_net_namespace())
> -             atomic_inc(&net->owner_ve->netif_failcount);
> +             ve_failcount_inc(net->owner_ve, netif);
> --- a/net/core/neighbour.c
> -                     atomic_inc(&ve->neigh_tbl_failcount);
> +                     ve_failcount_inc(ve, neigh_tbl);
> --- a/net/core/net_namespace.c
> -             atomic_inc(&ve->netns_failcount);
> +             ve_failcount_inc(ve, netns);
> 
> The idea behind it is to show in dmesg that failcount was reached, to simpify
> the detection of problematic containers for us.
> 
> For reference http://lore.virtuozzo.com/kernel/ccc3cbc8-e225-41cf-b563-699254aeb57b@virtuozzo.com/T/#t
> 

Fair, I'll do it in next version or as a follow-up if everything else is fine here.
Just instead of pr_warn we should use pr_warn_ratelimited, as it is easy
to trigger warning from container and flood dmesg and serial console.

>>   		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%]

* [Devel] [PATCH 1/1] ms/virtio_ring: fix infinite loop in virtnet_poll_cleantx when device is broken
@ 2026-08-31 15:16  3% Denis V. Lunev
  0 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-08-31 15:16 UTC (permalink / raw)
  To: devel

From: Jinqian Yang <yangjinqian1@huawei.com>

virtnet_poll_cleantx() contains a do-while loop that cleans up
transmitted TX buffers and calls virtqueue_enable_cb_delayed() to check
whether more buffers need processing. When the virtio backend stops
responding during guest reboot, used->idx is never updated, so
virtqueue_enable_cb_delayed() always returns false and the loop never
terminates. Then it will block reboot process, and the guest will hang.

The problem occurs during guest reboot under network traffic:

  1. kernel_restart() -> device_shutdown() traverses the device list
  2. virtio_dev_shutdown() calls virtio_break_device() which sets
     vq->broken = true
  3. virtio_dev_shutdown() then calls virtio_synchronize_cbs() to wait
     for in-flight callbacks to complete
  4. A virtio interrupt fires, softirq is deferred to ksoftirqd which
     calls net_rx_action() -> virtnet_poll() -> virtnet_poll_cleantx()
  5. virtnet_poll_cleantx() enters the do-while loop and never exits
     because the QEMU backend has stopped updating used->idx, despite
     vq->broken having been set to true in step 2.

Since the loop runs inside ksoftirqd (a SCHED_OTHER kthread), it is
visible to the scheduler and does not trigger a hard lockup. However,
the kthread never leaves the loop, so RCU detects it as a CPU stall
and reports it periodically. Meanwhile, the reboot process remains
blocked in device_shutdown() because virtio_dev_shutdown() cannot
complete its synchronization step, and the guest hangs permanently.

This can be reproduced on a guest with a virtio-net device: run iperf3
traffic in the guest, then trigger reboot. The reboot occasionally hangs
permanently with RCU stall on ksoftirqd.

Observed on ARM64 KVM guest:

  CPU#1 RCU stall (ksoftirqd/1), repeated periodically:
    virtqueue_enable_cb_delayed_split <- virtnet_poll <- __napi_poll <-
    net_rx_action <- handle_softirqs <- run_ksoftirqd <-
    smpboot_thread_fn <- kthread

Fix by adding a vq->broken check in virtqueue_enable_cb_delayed(), so
that the loop exits immediately when the device is broken, allowing
the device shutdown to proceed.

Signed-off-by: Jinqian Yang <yangjinqian1@huawei.com>
Reviewed-by: Xuan Zhuo <xuanzhuo@linux.alibaba.com>
Signed-off-by: Michael S. Tsirkin <mst@redhat.com>
Message-ID: <20260716115940.394832-1-yangjinqian1@huawei.com>

(cherry picked from commit 0d0eff39ceb3dcbf7847a6f4517086c207c60081)

Applied by hand: this tree has no data_race() annotation on
vq->event_triggered and dispatches the split and packed helpers
directly, so only the surrounding context differs. The added check is
identical.

This tree is hit harder than the kernel the fix was written against.
Upstream commit e13b6da7045f ("virtio-net: tweak for better TX
performance in NAPI mode") replaced the same do-while loop in
start_xmit() with a single check and is not here yet, so the livelock
is also reachable from a timer softirq: a delack timer transmitting on
a broken queue never returns from start_xmit(), and softlockup_panic
turns that into a panic rather than an RCU stall on ksoftirqd.

https://virtuozzo.atlassian.net/browse/VSTOR-143525
Feature: fix ms/virtio_ring
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 drivers/virtio/virtio_ring.c | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/drivers/virtio/virtio_ring.c b/drivers/virtio/virtio_ring.c
index 65674af37918..8097e49456d3 100644
--- a/drivers/virtio/virtio_ring.c
+++ b/drivers/virtio/virtio_ring.c
@@ -2529,6 +2529,14 @@ bool virtqueue_enable_cb_delayed(struct virtqueue *_vq)
 {
 	struct vring_virtqueue *vq = to_vvq(_vq);
 
+	/*
+	 * When the device is broken there is no point in polling used->idx,
+	 * the backend will never update it. Return true to let callers
+	 * exit their cleanup loops instead of spinning forever.
+	 */
+	if (unlikely(vq->broken))
+		return true;
+
 	if (vq->event_triggered)
 		vq->event_triggered = false;
 
-- 
2.53.0

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

^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH vz10] ms/selftests/sched_ext: flush stdout before test to avoid log spam
@ 2026-08-31 22:44  4% Eva Kurchatova
  0 siblings, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-08-31 22:44 UTC (permalink / raw)
  To: khorenko; +Cc: devel

From: Emil Tsalapatis <etsal@meta.com>

The sched_ext selftests runner runs each test in the same process,
with each test possibly forking multiple times. When the main runner
has not flushed its stdout, the children inherit the buffered output
for previous tests and emit it during exit. This causes log spam.

Make sure stdout/stderr is fully flushed before each test.

Cc: Ihor Solodrai <ihor.solodrai@linux.dev>
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
Signed-off-by: Tejun Heo <tj@kernel.org>
Started by hand, the runner writes 12 MB for its 23 tests, the preamble
of each repeated about five thousand times, against 8 KB with this
applied. run_kselftest.sh starts every test under stdbuf --output=L,
so a run through it never shows the duplication.

(cherry picked from commit 579a3297b268f0281644ead7ff574a2b4bc64d3c)

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

diff --git a/tools/testing/selftests/sched_ext/runner.c b/tools/testing/selftests/sched_ext/runner.c
index aa2d7d32dda9..5748d2c69903 100644
--- a/tools/testing/selftests/sched_ext/runner.c
+++ b/tools/testing/selftests/sched_ext/runner.c
@@ -46,6 +46,14 @@ static void print_test_preamble(const struct scx_test *test, bool quiet)
 	if (!quiet)
 		printf("DESCRIPTION: %s\n", test->description);
 	printf("OUTPUT:\n");
+
+	/*
+	 * The tests may fork with the preamble buffered
+	 * in the children's stdout. Flush before the test
+	 * to avoid printing the message multiple times.
+	 */
+	fflush(stdout);
+	fflush(stderr);
 }
 
 static const char *status_to_result(enum scx_test_status status)
-- 
2.55.0

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

^ permalink raw reply	[relevance 4%]

* [Devel] [PATCH vz10 1/3] ms/selftests/posix_timers: Use CLOCK_THREAD_CPUTIME_ID for ITIMER_PROF measurements
@ 2026-08-31 22:45  4% Eva Kurchatova
  0 siblings, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-08-31 22:45 UTC (permalink / raw)
  To: khorenko; +Cc: devel

From: John Stultz <jstultz@google.com>

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

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

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

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

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

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

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

(cherry picked from commit b00385b8d081ce74f36ea178e04e1b106505fb36)

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

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

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

^ permalink raw reply	[relevance 4%]

* [Devel] [PATCH vz10 3/7] selftests: net: skip what this kernel and iproute2 do not have
  @ 2026-08-31 22:48  2% ` Eva Kurchatova
  2026-08-31 22:48 16% ` [Devel] [PATCH vz10 7/7] 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-08-31 22:48 UTC (permalink / raw)
  To: khorenko; +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 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");
 	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 7/7] selftests: net: let the bridged PMTU tests take the ICMP they ask for
    2026-08-31 22:48  2% ` [Devel] [PATCH vz10 3/7] selftests: net: skip what this kernel and iproute2 do not have Eva Kurchatova
@ 2026-08-31 22:48 16% ` Eva Kurchatova
  1 sibling, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-08-31 22:48 UTC (permalink / raw)
  To: khorenko; +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/2] ms/selftests/damon/damon_nr_regions: sort collected regiosn before checking with min/max boundaries
  @ 2026-08-31 23:13  4% ` Eva Kurchatova
  0 siblings, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-08-31 23:13 UTC (permalink / raw)
  To: khorenko; +Cc: devel

From: SeongJae Park <sj@kernel.org>

damon_nr_regions.py starts DAMON, periodically collect number of regions
in snapshots, and see if it is in the requested range.  The check code
assumes the numbers are sorted on the collection list, but there is no
such guarantee.  Hence this can result in false positive test success.
Sort the list before doing the check.

Link: https://lkml.kernel.org/r/20250225222333.505646-4-sj@kernel.org
Fixes: 781497347d1b ("selftests/damon: implement test for min/max_nr_regions")
Signed-off-by: SeongJae Park <sj@kernel.org>
Cc: Shuah Khan <shuah@kernel.org>
Cc: <stable@vger.kernel.org>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
(cherry picked from commit 582ccf78f6090d88b1c7066b1e90b3d9ec952d08)

https://virtuozzo.atlassian.net/browse/VSTOR-132453
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/damon/damon_nr_regions.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tools/testing/selftests/damon/damon_nr_regions.py b/tools/testing/selftests/damon/damon_nr_regions.py
index 2ed12281cea2..29523275c262 100755
--- a/tools/testing/selftests/damon/damon_nr_regions.py
+++ b/tools/testing/selftests/damon/damon_nr_regions.py
@@ -69,6 +69,7 @@ def test_nr_regions(real_nr_regions, min_nr_regions, max_nr_regions):
 
     test_name = 'nr_regions test with %d/%d/%d real/min/max nr_regions' % (
             real_nr_regions, min_nr_regions, max_nr_regions)
+    collected_nr_regions.sort()
     if (collected_nr_regions[0] < min_nr_regions or
         collected_nr_regions[-1] > max_nr_regions):
         print('fail %s' % test_name)
-- 
2.55.0

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

^ permalink raw reply	[relevance 4%]

* Re: [Devel] [PATCH vz10] selftests/damon: wait for the merge to apply the new max_nr_regions
  2026-08-21 15:13 13% [Devel] [PATCH vz10] selftests/damon: wait for the merge to apply the new max_nr_regions Eva Kurchatova
@ 2026-08-31 23:22  0% ` Eva Kurchatova (Virtuozzo)
  0 siblings, 0 replies; 200+ results
From: Eva Kurchatova (Virtuozzo) @ 2026-08-31 23:22 UTC (permalink / raw)
  To: devel


On 8/21/26 18:13, Eva Kurchatova wrote:
> damon_nr_regions sleeps 0.3s after committing max_nr_regions and then
> reads the number of regions back, which assumes the merge has happened
> by then.  On a machine that is not idle it has not, and the test fails
> on a region count that is still the old one.
>
> Poll until the count settles instead of sleeping for a fixed time.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-132453
> Feature: fix selftests
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
> ---
>   .../selftests/damon/damon_nr_regions.py       | 35 +++++++++++--------
>   1 file changed, 20 insertions(+), 15 deletions(-)
>
> diff --git a/tools/testing/selftests/damon/damon_nr_regions.py b/tools/testing/selftests/damon/damon_nr_regions.py
> index cb31cd140d22..39063ce5b27d 100755
> --- a/tools/testing/selftests/damon/damon_nr_regions.py
> +++ b/tools/testing/selftests/damon/damon_nr_regions.py
> @@ -4,6 +4,7 @@
>   import subprocess
>   import time
>   
> +
>   import _damon_sysfs
>   
>   def test_nr_regions(real_nr_regions, min_nr_regions, max_nr_regions):
> @@ -114,27 +115,31 @@ def main():
>           proc.terminate()
>           print('commit failed: %s' % err)
>           exit(1)
> -    # wait for next merge operation is executed
> -    time.sleep(0.3)
> +    # wait for the merge operations to apply the new max_nr_regions
> +    nr_tried_regions = 0
> +    for _ in range(50):
> +        time.sleep(0.1)
>   
> -    err = kdamonds.kdamonds[0].update_schemes_tried_regions()
> -    if err is not None:
> -        proc.terminate()
> -        print('tried regions update failed: %s' % err)
> -        exit(1)
> +        err = kdamonds.kdamonds[0].update_schemes_tried_regions()
> +        if err is not None:
> +            proc.terminate()
> +            print('tried regions update failed: %s' % err)
> +            exit(1)
>   
> -    scheme = kdamonds.kdamonds[0].contexts[0].schemes[0]
> -    if scheme.tried_regions is None:
> -        proc.terminate()
> -        print('tried regions is not collected')
> -        exit(1)
> +        scheme = kdamonds.kdamonds[0].contexts[0].schemes[0]
> +        if scheme.tried_regions is None:
> +            proc.terminate()
> +            print('tried regions is not collected')
> +            exit(1)
> +
> +        nr_tried_regions = len(scheme.tried_regions)
> +        if 0 < nr_tried_regions <= 7:
> +            break
> +    proc.terminate()
>   
> -    nr_tried_regions = len(scheme.tried_regions)
>       if nr_tried_regions <= 0:
> -        proc.terminate()
>           print('tried regions is not created')
>           exit(1)
> -    proc.terminate()
>   
>       if nr_tried_regions > 7:
>           print('fail online-tuned max_nr_regions: %d > 7' % nr_tried_regions)

Sent v2, replacing this commit with two upstream backports: 
https://lists.openvz.org/pipermail/devel/2026-September/084959.html

Verification: 
https://bitbucket.org/virtuozzocore/vzkernel.vzspecs/pipelines/results/609


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

^ permalink raw reply	[relevance 0%]

* [Devel] [PATCH vz10 3/3] selftests: drv-net: read the channel count over netlink
  @ 2026-08-31 23:43  7% ` Eva Kurchatova
  0 siblings, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-08-31 23:43 UTC (permalink / raw)
  To: khorenko; +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>
---
 tools/testing/selftests/drivers/net/napi_threaded.py | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/tools/testing/selftests/drivers/net/napi_threaded.py b/tools/testing/selftests/drivers/net/napi_threaded.py
index 20f1a12552da..6662aee3442b 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 @@ 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 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.
+    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 7%]

* [Devel] [PATCH vz10] selftests: pstore: skip when no backend is registered
@ 2026-08-31 23:48  4% Eva Kurchatova
  2026-09-02 14:29  0% ` Konstantin Khorenko
  0 siblings, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-08-31 23:48 UTC (permalink / raw)
  To: khorenko; +Cc: devel

Without a pstore backend the console, /dev/pmsg0 and every write check
fail.

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 | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/tools/testing/selftests/pstore/common_tests b/tools/testing/selftests/pstore/common_tests
index 4509f0cc9c91..e486a5a111d8 100755
--- a/tools/testing/selftests/pstore/common_tests
+++ b/tools/testing/selftests/pstore/common_tests
@@ -55,6 +55,7 @@ operate_files() { # tested value, files, operation
 
 # Parameters
 TEST_STRING_PATTERN="Testing pstore: uuid="
+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}/
@@ -81,3 +82,7 @@ prlog -e "\tcmdline=`cat /proc/cmdline`"
 if [ $rc -ne 0 ]; then
     exit 1
 fi
+if [ -z "$backend" -o "$backend" = "(null)" ]; then
+    prlog "pstore backend is not registered, skipping"
+    exit $ksft_skip
+fi
-- 
2.55.0

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

^ permalink raw reply	[relevance 4%]

* [Devel] [PATCH vz10] selftests: pci_endpoint: skip when the test device is absent
@ 2026-08-31 23:47  3% Eva Kurchatova
  2026-09-02 14:57  4% ` Konstantin Khorenko
  0 siblings, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-08-31 23:47 UTC (permalink / raw)
  To: khorenko; +Cc: devel

Every fixture opens /dev/pci-endpoint-test.0 without checking, so on a
machine that has no PCI endpoint test device each case fails on the
first ioctl against fd -1 rather than saying the hardware is not there.

Check for the device once before handing over to the harness. Doing it
there rather than in each of the four fixtures keeps it to a single skip.

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      | 13 ++++++++++++-
 1 file changed, 12 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..640c74fd5b78 100644
--- a/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c
+++ b/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c
@@ -257,4 +257,15 @@ 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)
+{
+	if (!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] selftests: dma-buf: skip the huge page test without huge pages
@ 2026-09-01  0:41  3% Eva Kurchatova
  0 siblings, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-09-01  0:41 UTC (permalink / raw)
  To: khorenko; +Cc: devel

The last udmabuf test backs a memfd with MFD_HUGETLB. Where no huge
pages are reserved the mapping fails and the whole test is reported as
a failure.

https://virtuozzo.atlassian.net/browse/VSTOR-142448
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 .../selftests/drivers/dma-buf/udmabuf.c       | 20 +++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/tools/testing/selftests/drivers/dma-buf/udmabuf.c b/tools/testing/selftests/drivers/dma-buf/udmabuf.c
index 6062723a172e..a3bb26c1991a 100644
--- a/tools/testing/selftests/drivers/dma-buf/udmabuf.c
+++ b/tools/testing/selftests/drivers/dma-buf/udmabuf.c
@@ -25,6 +25,21 @@
 
 static unsigned int page_size;
 
+static bool hugepages_available(void)
+{
+	unsigned long nr = 0;
+	FILE *f;
+
+	f = fopen("/proc/sys/vm/nr_hugepages", "r");
+	if (!f)
+		return false;
+	if (fscanf(f, "%lu", &nr) != 1)
+		nr = 0;
+	fclose(f);
+
+	return nr > 0;
+}
+
 static int create_memfd_with_seals(off64_t size, bool hpage)
 {
 	int memfd, ret;
@@ -234,6 +249,11 @@ int main(int argc, char *argv[])
 	close(memfd);
 
 	/* should work (migration of 2MB size huge pages)*/
+	if (!hugepages_available()) {
+		ksft_test_result_skip("%s: [SKIP,test-6] no huge pages\n", TEST_PREFIX);
+		close(devfd);
+		ksft_finished();
+	}
 	page_size = getpagesize() * 512; /* 2 MB */
 	size = MEMFD_SIZE * page_size;
 	memfd = create_memfd_with_seals(size, true);
-- 
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] fs/fuse kio: track pending kRPC connect via state machine only
  2026-08-27 12:37  3% [Devel] [PATCH VZ10] fs/fuse kio: track pending kRPC connect via state machine only Liu Kui
  2026-08-28 17:13  0% ` Konstantin Khorenko
@ 2026-09-01 20:59  0% ` Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-01 20:59 UTC (permalink / raw)
  To: Liu Kui; +Cc: devel, azaitsev

> Rework the previous fix ("fs/fuse kio: fix kRPC connect issues") to not
> require the new struct pcs_krpc member "connect_req", so the fix can be
> shipped as a livepatch.
> 
> Both things connect_req was tracking are already derivable from the
> existing state machine once PCS_KRPC_STATE_CONNECT is made to mean
> exactly "a connect req is in flight":
> 
>  - krpc_connect_done() settles a failed connect back to UNCONN instead
>    of leaving the state in CONNECT forever;
> 
>  - pcs_krpc_abort() no longer resets CONNECT to UNCONN: the req is
>    still in flight, and only its completion settles the state;
> 
>  - pcs_krpc_connect() proceeds only from UNCONN or ABORTED, refusing
>    new connects (-EPERM) while a req is in flight - at most one connect
>    req exists at a time, same as with the connect_req check;
> 
>  - pcs_krpc_poll() reports EPOLLERR on UNCONN: poll bails out earlier
>    unless ctx->gen == krpc->gen, and the current session can only be in
>    UNCONN if its connect failed or was aborted, which is what the
>    (CONNECT && !connect_req) test used to detect.
> 
> gen only advances in pcs_krpc_connect(), which is blocked during
> CONNECT, so within that state the in-flight req always carries the
> current gen and krpc_connect_done()'s existing staleness check is
> sufficient.
> 
> Related to:
> https://virtuozzo.atlassian.net/browse/VSTOR-135626
> Fixes: d0d6034c36010 ("fs/fuse kio: fix kRPC connect issues")
> Feature: fuse: kRPC - single RPC for kernel and userspace
> 
> Signed-off-by: Liu Kui <kui.liu@virtuozzo.com>
>
> diff --git a/fs/fuse/kio/pcs/pcs_krpc.c b/fs/fuse/kio/pcs/pcs_krpc.c
> index 0930fb4adf125..f9fb6b3699062 100644
> --- a/fs/fuse/kio/pcs/pcs_krpc.c
> +++ b/fs/fuse/kio/pcs/pcs_krpc.c
> @@ -787,8 +787,12 @@ static int pcs_krpc_abort(struct pcs_krpc *krpc)
>       spin_lock(&krpc->lock);
>
>       if (krpc->state != PCS_KRPC_STATE_CONNECTED) {
> -             if (krpc->state == PCS_KRPC_STATE_CONNECT)
> -                     krpc->state = PCS_KRPC_STATE_UNCONN;
> +             /*
> +              * A pending connect stays in CONNECT state: its connect req
> +              * is still in flight and krpc_connect_done() will settle the
> +              * state to UNCONN when it completes.  Until then new connects
> +              * are refused, so at most one connect req exists at a time.
> +              */
>               spin_unlock(&krpc->lock);
>               return 0;
>       }

[Severity: High]
Can this change leave a krpc stuck in the connected state with no
session fd attached to it?

Consider a connect req in flight (state is CONNECT) whose fd is closed
by userspace after its connect timeout expires - the scenario the
original fix was written for:

pcs_krpc_release()
    if (ctx->gen == krpc->gen)
        pcs_krpc_abort(krpc);    /* state is CONNECT: does nothing now */

gen cannot advance while the state stays CONNECT, because
pcs_krpc_connect() refuses new connects with -EPERM in that state.  So
when the in-flight req later completes successfully (the peer became
reachable again), krpc_connect_done() passes its staleness check and
commits the dead session:

    if (req->gen != krpc->gen || krpc->state != PCS_KRPC_STATE_CONNECT) {
        spin_unlock(&krpc->lock);
        goto out;
    }

    if (!pcs_if_error(&msg->error)) {
        krpc->state = PCS_KRPC_STATE_CONNECTED;

Now the krpc is CONNECTED while its gen still names a session whose fd
is gone.  Every following PCS_IOC_KRPC_CONNECT returns -EPERM, there is
no fd left on which userspace could issue PCS_KRPC_IOC_ABORT, and no
kernel path resets the state, so the node stays unconnectable until the
krpc is destroyed.

The connect_req based code handled this case: pcs_krpc_abort() moved
CONNECT to UNCONN, a successful krpc_connect_done() then took the stale
path without transitioning to CONNECTED, and the next connect was
allowed as soon as the old req completed.

Does the abort/release path need to invalidate the pending connect, so
that a late successful completion settles the state to UNCONN instead
of resurrecting the closed session?

> @@ -956,8 +960,13 @@ static __poll_t pcs_krpc_poll(struct file *file, poll_table *wait)
>
>       spin_lock(&krpc->lock);
>
> +     /*
> +      * ctx->gen == krpc->gen (checked above) means this is the current
> +      * session, so UNCONN here can only mean its connect attempt has
> +      * failed (see krpc_connect_done()) or the session was aborted.
> +      */
>       if (krpc->state == PCS_KRPC_STATE_ABORTED ||
> -         (krpc->state == PCS_KRPC_STATE_CONNECT && !krpc->connect_req)) {
> +         krpc->state == PCS_KRPC_STATE_UNCONN) {
>               pollflags |= EPOLLERR;
>       } else if (krpc->state == PCS_KRPC_STATE_CONNECTED) {
>               pollflags |= EPOLLOUT;

[Severity: Low]
This isn't a bug, but after this change pcs_krpc_abort() never sets
UNCONN, and an aborted session is left in ABORTED, which the first
check already handles.

For a session with ctx->gen == krpc->gen, can UNCONN still be reached
through an abort as the comment says, or only through a failed
connect?  The commit message carries the same wording ("the current
session can only be in UNCONN if its connect failed or was aborted").

[ ... ]

-- 
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] selftests: pstore: skip when no backend is registered
  2026-08-31 23:48  4% [Devel] [PATCH vz10] selftests: pstore: skip when no backend is registered Eva Kurchatova
@ 2026-09-02 14:29  0% ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-02 14:29 UTC (permalink / raw)
  To: Eva Kurchatova; +Cc: devel

> Without a pstore backend the console, /dev/pmsg0 and every write check
> fail.
> 
> 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..e486a5a111d8d 100755
> --- a/tools/testing/selftests/pstore/common_tests
> +++ b/tools/testing/selftests/pstore/common_tests
> @@ -55,6 +55,7 @@ operate_files() { # tested value, files, operation
>  
>  # Parameters
>  TEST_STRING_PATTERN="Testing pstore: uuid="
> +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}/
> @@ -81,3 +82,7 @@ prlog -e "\tcmdline=`cat /proc/cmdline`"
>  if [ $rc -ne 0 ]; then
>      exit 1
>  fi
> +if [ -z "$backend" -o "$backend" = "(null)" ]; then
> +    prlog "pstore backend is not registered, skipping"
> +    exit $ksft_skip
> +fi

The goal is right, and it is actually what this test set has claimed to do
since 2015: pstore_crash_test:10 carries the comment

      # exit if pstore backend is not registered
      . ./common_tests

but common_tests never implemented it. The existing check at
common_tests:77-79 tests cat's exit status, i.e. only whether the sysfs file
is readable, not what it contains. So the patch finally implements what the
comment promises. Upstream common_tests is identical to ours, so this is
worth sending to mainline too.

================================================
And send to mainstream in parallel, do not wait!
================================================

1) The output now contradicts itself:

      Checking pstore backend is registered ... ok
              backend=(null)
              cmdline=...
      pstore backend is not registered, skipping

Since the patch is about exactly this check, please fix its verdict instead
of appending a second check at the end of the file. Keep the backend= and
cmdline= diagnostics - they are useful in the skip case too.

2) CONFIG_PSTORE=n (and pstore built as a module but not loaded) still FAILs
instead of skipping. CONFIG_PSTORE is tristate (fs/pstore/Kconfig:3), so
/sys/module/pstore/parameters/backend may not exist at all; then cat fails,
show_result reports FAIL, rc=1, and common_tests:82-84 exits 1 before the new
check is reached. That is the most direct "feature not built" case and it
should skip too. One extra condition.

3) More important in practice: on our config the patch is not enough once a
backend does register. We have

      CONFIG_PSTORE=y
      # CONFIG_PSTORE_CONSOLE is not set
      # CONFIG_PSTORE_PMSG is not set
      CONFIG_PSTORE_RAM=m
      CONFIG_EFI_VARS_PSTORE=y
      CONFIG_EFI_VARS_PSTORE_DEFAULT_DISABLE=y

while pstore_tests checks precisely the frontends we do not have: the pstore
console (pstore_tests:13) and /dev/pmsg0 (:17, :21). The test's own config
file asks for CONFIG_PSTORE_PMSG=y and CONFIG_PSTORE_CONSOLE=y.

What saves us today is only that no backend registers by default:
efi_pstore is off (pstore_disable = IS_ENABLED(CONFIG_EFI_VARS_PSTORE_DEFAULT_DISABLE),
drivers/firmware/efi/efi-pstore.c:22, early return at :261), ERST registers
only if the platform provides an ERST range (drivers/acpi/apei/erst.c:1266),
and ramoops is a module that is not autoloaded. Hence backend=(null) and the
skip fires.

But on hardware with ERST, or when booted with efivars.pstore_disable=0, or
after modprobe ramoops, pstore_tests fails again - for a reason unrelated to
the backend. If the goal is a green run under any conditions, the frontend
checks need their own skips: the console check only when a pstore console is
actually registered, the pmsg checks only when /dev/pmsg0 exists. That can be
a separate patch, but it should be decided now, otherwise the task gets
closed while the test still fails on some machines.

Nits:

- [ -z "$backend" -o "$backend" = "(null)" ]: -o inside [ is obsolescent in
  POSIX; prefer [ -z "$backend" ] || [ "$backend" = "(null)" ].
- pstore_post_reboot_tests:10-11 has the canonical comment above the same
  variable ("# Kselftest framework requirement - SKIP code is 4."); worth
  repeating it. After that the duplicate ksft_skip=4 in
  pstore_post_reboot_tests can be dropped, since it sources common_tests
  (line 13) before first using the variable (line 19).

-- 
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] selftests: pci_endpoint: skip when the test device is absent
  2026-08-31 23:47  3% [Devel] [PATCH vz10] selftests: pci_endpoint: skip when the test device is absent Eva Kurchatova
@ 2026-09-02 14:57  4% ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-02 14:57 UTC (permalink / raw)
  To: Eva Kurchatova; +Cc: devel

> Every fixture opens /dev/pci-endpoint-test.0 without checking, so on a
> machine that has no PCI endpoint test device each case fails on the
> first ioctl against fd -1 rather than saying the hardware is not there.
> 

The commit message describes a different bug than the one that exists:

      Every fixture opens /dev/pci-endpoint-test.0 without checking, so on a
      machine that has no PCI endpoint test device each case fails on the
      first ioctl against fd -1

Every fixture does check:

      FIXTURE_SETUP(pci_ep_bar)
      {
              self->fd = open(test_device, O_RDWR);

              ASSERT_NE(-1, self->fd) TH_LOG("Can't open PCI Endpoint Test device");
      }

at pci_endpoint_test.c:43 (pci_ep_bar), :82 (pci_ep_basic), :153
(pci_ep_data_transfer) and :232 (pcie_ep_doorbell).

No ioctl is ever reached - what fails is the assertion in the fixture setup
("Test terminated by assertion" in the output above).

Please reword: each of the 17 cases fails on the fixture setup assertion.
Otherwise the next reader goes looking for an ioctl(-1, ...) bug that is not
there. The "four fixtures" part is correct.

> Check for the device once before handing over to the harness. Doing it
> there rather than in each of the four fixtures keeps it to a single skip.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-142446
> Feature: fix selftests
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
>
> diff --git a/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c b/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c
> index da0db0e7c9693..640c74fd5b780 100644
> --- a/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c
> +++ b/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c
> @@ -257,4 +257,15 @@ 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

An empty line to be here.

> +static bool test_device_available(void)
> +{
> +	return access(test_device, F_OK) == 0;
> +}
> +
> +int main(int argc, char **argv)
> +{
> +	if (!test_device_available())
> +		ksft_exit_skip("no PCI endpoint test device %s\n", test_device);
> +
> +	return test_harness_run(argc, argv);
> +}

The check runs before test_harness_run(), while argv parsing happens
inside it (test_harness_argv_check(), kselftest_harness.h:1119).

So on a machine without the device, "./pci_endpoint_test -l" (list tests) and
"-h" (usage) now print the skip message instead of their own output. Please
limit the skip to the plain invocation, which is the one kselftest uses:

      int main(int argc, char **argv)
      {
              /*
               * Only the plain invocation - 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);
      }

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

^ permalink raw reply	[relevance 4%]

* [QEMU HCI-8.0 PATCH 3/5] vhost-blk: add read-only flag
    2026-09-03 12:32 24% ` [QEMU HCI-8.0 PATCH 2/5] vhost-blk: " Andrey Zhadchenko
@ 2026-09-03 12:32 26% ` Andrey Zhadchenko
  2026-09-03 14:56  6%   ` Andrey Drobyshev
  2026-09-03 12:32 20% ` [QEMU HCI-8.0 PATCH 4/5] vhost-blk: watch the device for resize events Andrey Zhadchenko
  2026-09-03 12:32  3% ` [QEMU HCI-8.0 PATCH 5/5] vhost-blk: filter uevents in the kernel Andrey Zhadchenko
  3 siblings, 1 reply; 200+ results
From: Andrey Zhadchenko @ 2026-09-03 12:32 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

and set RO respectively. Also compare BLKROGET with the selected
mode and reject r/w if needed.

https://virtuozzo.atlassian.net/browse/VSTOR-143437
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
 hw/block/vhost-blk.c          | 23 ++++++++++++++++++++++-
 include/hw/virtio/vhost-blk.h |  1 +
 2 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index a3e0010982..47b6e560d4 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -24,6 +24,7 @@
 #include "system/system.h"
 #include "linux-headers/linux/vhost.h"
 #include <sys/ioctl.h>
+#include <linux/fs.h>
 #include "system/runstate.h"
 
 static int vhost_blk_start(VirtIODevice *vdev)
@@ -233,8 +234,10 @@ static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
 {
     BlockConf *conf = &s->conf.conf;
     struct stat st;
+    int readonly;
+    int open_flags = s->conf.readonly ? O_RDONLY : O_RDWR;
 
-    s->backend_fd = qemu_open(s->conf.devpath, O_RDWR, errp);
+    s->backend_fd = qemu_open(s->conf.devpath, open_flags, errp);
     if (s->backend_fd < 0) {
         error_prepend(errp, "vhost-blk: unable to open backend: ");
         return false;
@@ -252,6 +255,19 @@ static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
         goto fail;
     }
 
+    if (ioctl(s->backend_fd, BLKROGET, &readonly) < 0) {
+        error_setg_errno(errp, errno,
+                         "vhost-blk: unable to get read-only status of '%s'",
+                         s->conf.devpath);
+        goto fail;
+    }
+
+    if (readonly && !s->conf.readonly) {
+        error_setg(errp, "vhost-blk: '%s' is read-only",
+                   s->conf.devpath);
+        goto fail;
+    }
+
     if (vhost_blk_update_size(s, errp) < 0) {
         goto fail;
     }
@@ -416,6 +432,10 @@ static uint64_t vhost_blk_get_features(VirtIODevice *vdev,
 
     virtio_add_feature(&features, VIRTIO_F_VERSION_1);
 
+    if (s->conf.readonly) {
+        virtio_add_feature(&features, VIRTIO_BLK_F_RO);
+    }
+
     if (s->conf.num_queues > 1) {
         virtio_add_feature(&features, VIRTIO_BLK_F_MQ);
     }
@@ -464,6 +484,7 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
 static const Property vhost_blk_properties[] = {
     DEFINE_BLOCK_PROPERTIES_BASE(VHostBlk, conf.conf),
     DEFINE_PROP_STRING("devpath", VHostBlk, conf.devpath),
+    DEFINE_PROP_BOOL("read-only", VHostBlk, conf.readonly, false),
     DEFINE_PROP_UINT16("num-queues", VHostBlk, conf.num_queues,
                        VHOST_BLK_AUTO_NUM_QUEUES),
     DEFINE_PROP_UINT16("queue-size", VHostBlk, conf.queue_size, 256),
diff --git a/include/hw/virtio/vhost-blk.h b/include/hw/virtio/vhost-blk.h
index c194b421d9..c6646f5845 100644
--- a/include/hw/virtio/vhost-blk.h
+++ b/include/hw/virtio/vhost-blk.h
@@ -25,6 +25,7 @@
 typedef struct VhostBlkConf {
     BlockConf conf;
     char *devpath;
+    bool readonly;
     uint16_t num_queues;
     uint16_t queue_size;
     uint16_t num_threads;
-- 
2.43.5


^ permalink raw reply	[relevance 26%]

* [QEMU HCI-8.0 PATCH 5/5] vhost-blk: filter uevents in the kernel
                     ` (2 preceding siblings ...)
  2026-09-03 12:32 20% ` [QEMU HCI-8.0 PATCH 4/5] vhost-blk: watch the device for resize events Andrey Zhadchenko
@ 2026-09-03 12:32  3% ` Andrey Zhadchenko
  3 siblings, 0 replies; 200+ results
From: Andrey Zhadchenko @ 2026-09-03 12:32 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

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


^ permalink raw reply	[relevance 3%]

* [QEMU HCI-8.0 PATCH 2/5] vhost-blk: change backend setup
  @ 2026-09-03 12:32 24% ` Andrey Zhadchenko
  2026-09-03 14:56  0%   ` Andrey Drobyshev
  2026-09-03 12:32 26% ` [QEMU HCI-8.0 PATCH 3/5] vhost-blk: add read-only flag Andrey Zhadchenko
                   ` (2 subsequent siblings)
  3 siblings, 1 reply; 200+ results
From: Andrey Zhadchenko @ 2026-09-03 12:32 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

Previously we used very ugly and incapsulation-breaking assignment
fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
It is wrong in a many ways, so let's rework this.

Patch changes default `drive` to new `devpath` option so device fd
is managed by vhost-blk itself. Unfortunately this way we need a
bit more preparational work: finding out disk length, block size,
etc. Don't be too broad and just do the minimal and set reasonable
default values. Validate with previously introduced
blkconf_validate_blocksizes().
Also we lose resize, as this is tied to the block node, which is
now have no place in the setup. We will add this in the next
patches as well as RO mode.

https://virtuozzo.atlassian.net/browse/VSTOR-143437
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
 hw/block/vhost-blk.c          | 144 ++++++++++++++++++++++------------
 include/hw/virtio/vhost-blk.h |   5 +-
 2 files changed, 95 insertions(+), 54 deletions(-)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index c52851fcf8..a3e0010982 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -24,23 +24,17 @@
 #include "system/system.h"
 #include "linux-headers/linux/vhost.h"
 #include <sys/ioctl.h>
-#include <linux/fs.h>
-#include "include/block/block_int-common.h"
 #include "system/runstate.h"
 
 static int vhost_blk_start(VirtIODevice *vdev)
 {
     VHostBlk *s = VHOST_BLK(vdev);
     struct vhost_vring_file backend;
-    int ret, i, nworkers, *fd;
+    int ret, i, nworkers;
     BusState *qbus = BUS(qdev_get_parent_bus(DEVICE(vdev)));
     VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
     char serial[VIRTIO_BLK_ID_BYTES] = {0};
 
-    bdrv_graph_rdlock_main_loop();
-    fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
-    bdrv_graph_rdunlock_main_loop();
-
     if (!k->set_guest_notifiers) {
         error_report("vhost-blk: binding does not support guest notifiers");
         return -ENOSYS;
@@ -92,7 +86,7 @@ static int vhost_blk_start(VirtIODevice *vdev)
 
     memset(&backend, 0, sizeof(backend));
     backend.index = 0;
-    backend.fd = *fd;
+    backend.fd = s->backend_fd;
     if (ioctl(s->vhostfd, VHOST_BLK_SET_BACKEND, &backend)) {
         error_report("vhost-blk: unable to set backend");
         ret = -errno;
@@ -208,29 +202,79 @@ static void vhost_blk_vm_state(void *opaque, bool running, RunState state)
     }
 }
 
-static void vhost_blk_resize_cb(void *opaque)
+static int vhost_blk_update_size(VHostBlk *s, Error **errp)
 {
-    VirtIODevice *vdev = opaque;
+    BlockConf *conf = &s->conf.conf;
+    off_t length;
+    bool changed;
+
+    length = lseek(s->backend_fd, 0, SEEK_END);
+    if (length < 0) {
+        int error = errno;
+
+        error_setg_errno(errp, error,
+                         "vhost-blk: unable to determine size of '%s'",
+                         s->conf.devpath);
+        return -error;
+    }
 
-    assert(qemu_get_current_aio_context() == qemu_get_aio_context());
-    virtio_notify_config(vdev);
+    changed = s->length != length;
+    s->length = length;
+    conf->heads = 16;
+    conf->secs = 63;
+    conf->cyls = s->length / BDRV_SECTOR_SIZE /
+                 (conf->heads * conf->secs);
+    conf->cyls = MIN(MAX(conf->cyls, 2U), 16383U);
+
+    return changed;
 }
 
-static void vhost_blk_resize(void *opaque)
+static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
 {
-    VirtIODevice *vdev = VIRTIO_DEVICE(opaque);
+    BlockConf *conf = &s->conf.conf;
+    struct stat st;
 
-    /*
-     * virtio_notify_config() needs to acquire the global mutex,
-     * so it can't be called from an iothread. Instead, schedule
-     * it to be run in the main context BH.
-     */
-    aio_bh_schedule_oneshot(qemu_get_aio_context(), vhost_blk_resize_cb, vdev);
-}
+    s->backend_fd = qemu_open(s->conf.devpath, O_RDWR, errp);
+    if (s->backend_fd < 0) {
+        error_prepend(errp, "vhost-blk: unable to open backend: ");
+        return false;
+    }
 
-static const BlockDevOps vhost_blk_block_ops = {
-    .resize_cb     = vhost_blk_resize,
-};
+    if (fstat(s->backend_fd, &st) < 0) {
+        error_setg_errno(errp, errno, "vhost-blk: unable to stat '%s'",
+                         s->conf.devpath);
+        goto fail;
+    }
+
+    if (!S_ISBLK(st.st_mode)) {
+        error_setg(errp, "vhost-blk: '%s' is not a block device",
+                   s->conf.devpath);
+        goto fail;
+    }
+
+    if (vhost_blk_update_size(s, errp) < 0) {
+        goto fail;
+    }
+
+    if (!conf->logical_block_size) {
+        conf->logical_block_size = BDRV_SECTOR_SIZE;
+    }
+
+    if (!conf->physical_block_size) {
+        conf->physical_block_size = BDRV_SECTOR_SIZE;
+    }
+
+    if (!blkconf_validate_blocksizes(conf, errp)) {
+        goto fail;
+    }
+
+    return true;
+
+fail:
+    qemu_close(s->backend_fd);
+    s->backend_fd = -1;
+    return false;
+}
 
 static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
 {
@@ -239,13 +283,8 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
     VhostBlkConf *conf = &s->conf;
     int i, ret;
 
-    if (!conf->conf.blk) {
-        error_setg(errp, "vhost-blk: drive property not set");
-        return;
-    }
-
-    if (!blk_is_inserted(conf->conf.blk)) {
-        error_setg(errp, "vhost-blk: device needs media, but drive is empty");
+    if (!conf->devpath) {
+        error_setg(errp, "vhost-blk: devpath property must be set");
         return;
     }
 
@@ -273,17 +312,7 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
         return;
     }
 
-    if (!blkconf_apply_backend_options(&conf->conf,
-                                       !blk_supports_write_perm(conf->conf.blk),
-                                       true, errp)) {
-        return;
-    }
-
-    if (!blkconf_geometry(&conf->conf, NULL, 65535, 255, 255, errp)) {
-        return;
-    }
-
-    if (!blkconf_blocksizes(&conf->conf, errp)) {
+    if (!vhost_blk_open_backend(s, errp)) {
         return;
     }
 
@@ -311,13 +340,13 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
         goto cleanup;
     }
 
-    blk_set_dev_ops(s->conf.conf.blk, &vhost_blk_block_ops, s);
-
     ret = vhost_dev_init(&s->dev, (void *)((size_t)s->vhostfd),
                          VHOST_BACKEND_TYPE_KERNEL, 0, NULL);
     if (ret < 0) {
         error_setg(errp, "vhost-blk: vhost initialization failed: %s",
                 strerror(-ret));
+        /* vhost_dev_init() closes vhostfd on failure */
+        s->vhostfd = -1;
         goto cleanup;
     }
 
@@ -328,7 +357,14 @@ cleanup:
             qemu_del_vm_change_state_handler(s->mighand);
     }
     g_free(s->dev.vqs);
-    close(s->vhostfd);
+    if (s->vhostfd >= 0) {
+        close(s->vhostfd);
+        s->vhostfd = -1;
+    }
+    if (s->backend_fd >= 0) {
+        qemu_close(s->backend_fd);
+        s->backend_fd = -1;
+    }
     for (i = 0; i < conf->num_queues; i++) {
         virtio_del_queue(vdev, i);
     }
@@ -344,6 +380,10 @@ static void vhost_blk_device_unrealize(DeviceState *dev)
     qemu_del_vm_change_state_handler(s->mighand);
     vhost_blk_set_status(vdev, 0);
     vhost_dev_cleanup(&s->dev);
+    if (s->backend_fd >= 0) {
+        qemu_close(s->backend_fd);
+        s->backend_fd = -1;
+    }
     g_free(s->dev.vqs);
     virtio_cleanup(vdev);
 }
@@ -376,10 +416,6 @@ static uint64_t vhost_blk_get_features(VirtIODevice *vdev,
 
     virtio_add_feature(&features, VIRTIO_F_VERSION_1);
 
-    if (!blk_is_writable(s->conf.conf.blk)) {
-        virtio_add_feature(&features, VIRTIO_BLK_F_RO);
-    }
-
     if (s->conf.num_queues > 1) {
         virtio_add_feature(&features, VIRTIO_BLK_F_MQ);
     }
@@ -398,7 +434,9 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
     int64_t length;
     int blk_size = conf->logical_block_size;
 
-    blk_get_geometry(s->conf.conf.blk, &capacity);
+    length = s->length;
+    capacity = length / BDRV_SECTOR_SIZE;
+
     memset(&blkcfg, 0, sizeof(blkcfg));
     virtio_stq_p(vdev, &blkcfg.capacity, capacity);
     virtio_stl_p(vdev, &blkcfg.seg_max, s->conf.queue_size - 2);
@@ -406,7 +444,6 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
     virtio_stl_p(vdev, &blkcfg.blk_size, blk_size);
     blkcfg.geometry.heads = conf->heads;
 
-    length = blk_getlength(s->conf.conf.blk);
     if (length > 0 && length / conf->heads / conf->secs % blk_size) {
         unsigned short mask;
 
@@ -425,7 +462,8 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
 }
 
 static const Property vhost_blk_properties[] = {
-    DEFINE_BLOCK_PROPERTIES(VHostBlk, conf.conf),
+    DEFINE_BLOCK_PROPERTIES_BASE(VHostBlk, conf.conf),
+    DEFINE_PROP_STRING("devpath", VHostBlk, conf.devpath),
     DEFINE_PROP_UINT16("num-queues", VHostBlk, conf.num_queues,
                        VHOST_BLK_AUTO_NUM_QUEUES),
     DEFINE_PROP_UINT16("queue-size", VHostBlk, conf.queue_size, 256),
@@ -470,6 +508,8 @@ static void vhost_blk_instance_init(Object *obj)
 {
     VHostBlk *s = VHOST_BLK(obj);
 
+    s->vhostfd = -1;
+    s->backend_fd = -1;
     device_add_bootindex_property(obj, &s->conf.conf.bootindex,
                                   "bootindex", "/disk@0,0",
                                   DEVICE(obj));
diff --git a/include/hw/virtio/vhost-blk.h b/include/hw/virtio/vhost-blk.h
index 0c7e212595..c194b421d9 100644
--- a/include/hw/virtio/vhost-blk.h
+++ b/include/hw/virtio/vhost-blk.h
@@ -14,7 +14,6 @@
 #include "standard-headers/linux/virtio_blk.h"
 #include "hw/block/block.h"
 #include "hw/virtio/vhost.h"
-#include "system/block-backend.h"
 
 #define TYPE_VHOST_BLK "vhost-blk"
 #define VHOST_BLK(obj) \
@@ -25,6 +24,7 @@
 
 typedef struct VhostBlkConf {
     BlockConf conf;
+    char *devpath;
     uint16_t num_queues;
     uint16_t queue_size;
     uint16_t num_threads;
@@ -37,10 +37,11 @@ typedef struct VHostBlk {
     VMChangeStateEntry *mighand;
     uint64_t host_features;
     uint64_t decided_features;
-    struct virtio_blk_config blkcfg;
     int vhostfd;
+    int backend_fd;
     struct vhost_dev dev;
     bool vhost_started;
+    uint64_t length;
 } VHostBlk;
 
 #endif
-- 
2.43.5


^ permalink raw reply	[relevance 24%]

* [QEMU HCI-8.0 PATCH 4/5] vhost-blk: watch the device for resize events
    2026-09-03 12:32 24% ` [QEMU HCI-8.0 PATCH 2/5] vhost-blk: " Andrey Zhadchenko
  2026-09-03 12:32 26% ` [QEMU HCI-8.0 PATCH 3/5] vhost-blk: add read-only flag Andrey Zhadchenko
@ 2026-09-03 12:32 20% ` Andrey Zhadchenko
  2026-09-03 12:32  3% ` [QEMU HCI-8.0 PATCH 5/5] vhost-blk: filter uevents in the kernel Andrey Zhadchenko
  3 siblings, 0 replies; 200+ results
From: Andrey Zhadchenko @ 2026-09-03 12:32 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

Resize was tied to block node, which we removed some time ago.
Luckily we can make resize automated: watch netlink for relevant
events and call virtio_notify_config() if we detect capacity
change.
Failed netlink setup during creation leads to failure, but this
is a price we are ready to pay for consistency.

https://virtuozzo.atlassian.net/browse/VSTOR-143437
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
 hw/block/vhost-blk.c          | 193 ++++++++++++++++++++++++++++++++++
 include/hw/virtio/vhost-blk.h |   5 +
 2 files changed, 198 insertions(+)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index 47b6e560d4..f8eca4d58a 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -10,7 +10,9 @@
 
 #include "qemu/osdep.h"
 #include "qapi/error.h"
+#include "qemu/cutils.h"
 #include "qemu/error-report.h"
+#include "qemu/main-loop.h"
 #include "qom/object.h"
 #include "hw/qdev-core.h"
 #include "hw/boards.h"
@@ -25,8 +27,13 @@
 #include "linux-headers/linux/vhost.h"
 #include <sys/ioctl.h>
 #include <linux/fs.h>
+#include <linux/netlink.h>
 #include "system/runstate.h"
 
+static int vhost_blk_uevent_fd = -1;
+static QLIST_HEAD(, VHostBlk) vhost_blk_uevent_watchers =
+    QLIST_HEAD_INITIALIZER(vhost_blk_uevent_watchers);
+
 static int vhost_blk_start(VirtIODevice *vdev)
 {
     VHostBlk *s = VHOST_BLK(vdev);
@@ -230,6 +237,182 @@ static int vhost_blk_update_size(VHostBlk *s, Error **errp)
     return changed;
 }
 
+static void vhost_blk_resize_bh(void *opaque)
+{
+    VHostBlk *s = opaque;
+    Error *local_err = NULL;
+    int ret;
+
+    ret = vhost_blk_update_size(s, &local_err);
+    if (ret < 0) {
+        error_report_err(local_err);
+        return;
+    }
+
+    if (ret) {
+        virtio_notify_config(VIRTIO_DEVICE(s));
+    }
+}
+
+static void vhost_blk_uevent_read(void *opaque)
+{
+    char buffer[64 * 1024 + 1];
+
+    for (;;) {
+        struct sockaddr_nl source;
+        socklen_t source_len = sizeof(source);
+        uint64_t event_major = UINT64_MAX;
+        uint64_t event_minor = UINT64_MAX;
+        bool action_change = false;
+        bool subsystem_block = false;
+        bool resize = false;
+        char *field;
+        char *end;
+        ssize_t len;
+
+        memset(&source, 0, sizeof(source));
+        len = recvfrom(vhost_blk_uevent_fd, buffer, sizeof(buffer) - 1,
+                       MSG_DONTWAIT, (struct sockaddr *)&source, &source_len);
+        if (len < 0) {
+            if (errno == EINTR) {
+                continue;
+            }
+            if (errno == ENOBUFS) {
+                VHostBlk *s;
+
+                /* uevents dropped. Re-check just to be sure */
+                QLIST_FOREACH(s, &vhost_blk_uevent_watchers, uevent_node) {
+                    qemu_bh_schedule(s->resize_bh);
+                }
+                continue;
+            }
+            if (errno != EAGAIN && errno != EWOULDBLOCK) {
+                error_report("vhost-blk: unable to receive uevent: %s",
+                             strerror(errno));
+            }
+            return;
+        }
+
+        if (source.nl_family != AF_NETLINK || source.nl_pid != 0) {
+            continue;
+        }
+
+        buffer[len] = '\0';
+        field = buffer;
+        end = buffer + len;
+        while (field < end) {
+            size_t field_len = strnlen(field, end - field);
+
+            if (!strcmp(field, "ACTION=change")) {
+                action_change = true;
+            } else if (!strcmp(field, "SUBSYSTEM=block")) {
+                subsystem_block = true;
+            } else if (!strcmp(field, "RESIZE=1")) {
+                resize = true;
+            } else if (g_str_has_prefix(field, "MAJOR=")) {
+                uint64_t value;
+
+                if (!qemu_strtou64(field + strlen("MAJOR="), NULL, 10,
+                                   &value)) {
+                    event_major = value;
+                }
+            } else if (g_str_has_prefix(field, "MINOR=")) {
+                uint64_t value;
+
+                if (!qemu_strtou64(field + strlen("MINOR="), NULL, 10,
+                                   &value)) {
+                    event_minor = value;
+                }
+            }
+
+            if (field_len == end - field) {
+                break;
+            }
+            field += field_len + 1;
+        }
+
+        if (action_change && subsystem_block && resize &&
+            event_major <= UINT_MAX && event_minor <= UINT_MAX) {
+            VHostBlk *s;
+
+            QLIST_FOREACH(s, &vhost_blk_uevent_watchers, uevent_node) {
+                dev_t rdev = s->backend_rdev;
+
+                if (major(rdev) == event_major &&
+                    minor(rdev) == event_minor) {
+                    qemu_bh_schedule(s->resize_bh);
+                }
+            }
+        }
+    }
+}
+
+static bool vhost_blk_uevent_init(Error **errp)
+{
+    struct sockaddr_nl address = {
+        .nl_family = AF_NETLINK,
+        .nl_groups = 1,
+    };
+
+    if (vhost_blk_uevent_fd >= 0) {
+        return true;
+    }
+
+    vhost_blk_uevent_fd = socket(AF_NETLINK,
+                                 SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC,
+                                 NETLINK_KOBJECT_UEVENT);
+    if (vhost_blk_uevent_fd < 0) {
+        error_setg_errno(errp, errno,
+                         "vhost-blk: unable to create uevent socket");
+        return false;
+    }
+
+    if (bind(vhost_blk_uevent_fd, (struct sockaddr *)&address,
+             sizeof(address)) < 0) {
+        error_setg_errno(errp, errno,
+                         "vhost-blk: unable to bind uevent socket");
+        qemu_close(vhost_blk_uevent_fd);
+        vhost_blk_uevent_fd = -1;
+        return false;
+    }
+
+    qemu_set_fd_handler(vhost_blk_uevent_fd, vhost_blk_uevent_read,
+                        NULL, NULL);
+    return true;
+}
+
+static void vhost_blk_uevent_cleanup_if_unused(void)
+{
+    if (vhost_blk_uevent_fd < 0 ||
+        !QLIST_EMPTY(&vhost_blk_uevent_watchers)) {
+        return;
+    }
+
+    qemu_set_fd_handler(vhost_blk_uevent_fd, NULL, NULL, NULL);
+    qemu_close(vhost_blk_uevent_fd);
+    vhost_blk_uevent_fd = -1;
+}
+
+static void vhost_blk_uevent_register(VHostBlk *s)
+{
+    s->resize_bh = qemu_bh_new(vhost_blk_resize_bh, s);
+    QLIST_INSERT_HEAD(&vhost_blk_uevent_watchers, s, uevent_node);
+    s->uevent_registered = true;
+}
+
+static void vhost_blk_uevent_unregister(VHostBlk *s)
+{
+    if (!s->uevent_registered) {
+        return;
+    }
+
+    QLIST_REMOVE(s, uevent_node);
+    s->uevent_registered = false;
+    qemu_bh_delete(s->resize_bh);
+    s->resize_bh = NULL;
+    vhost_blk_uevent_cleanup_if_unused();
+}
+
 static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
 {
     BlockConf *conf = &s->conf.conf;
@@ -254,6 +437,7 @@ static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
                    s->conf.devpath);
         goto fail;
     }
+    s->backend_rdev = st.st_rdev;
 
     if (ioctl(s->backend_fd, BLKROGET, &readonly) < 0) {
         error_setg_errno(errp, errno,
@@ -332,6 +516,12 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
         return;
     }
 
+    if (!vhost_blk_uevent_init(errp)) {
+        qemu_close(s->backend_fd);
+        s->backend_fd = -1;
+        return;
+    }
+
     s->dev.nvqs = conf->num_queues;
     s->dev.max_queues = conf->num_queues;
     s->dev.vqs = g_new0(struct vhost_virtqueue, s->dev.nvqs);
@@ -366,6 +556,7 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
         goto cleanup;
     }
 
+    vhost_blk_uevent_register(s);
     return;
 
 cleanup:
@@ -385,6 +576,7 @@ cleanup:
         virtio_del_queue(vdev, i);
     }
     virtio_cleanup(vdev);
+    vhost_blk_uevent_cleanup_if_unused();
     return;
 }
 
@@ -393,6 +585,7 @@ static void vhost_blk_device_unrealize(DeviceState *dev)
     VirtIODevice *vdev = VIRTIO_DEVICE(dev);
     VHostBlk *s = VHOST_BLK(dev);
 
+    vhost_blk_uevent_unregister(s);
     qemu_del_vm_change_state_handler(s->mighand);
     vhost_blk_set_status(vdev, 0);
     vhost_dev_cleanup(&s->dev);
diff --git a/include/hw/virtio/vhost-blk.h b/include/hw/virtio/vhost-blk.h
index c6646f5845..815939419f 100644
--- a/include/hw/virtio/vhost-blk.h
+++ b/include/hw/virtio/vhost-blk.h
@@ -14,6 +14,7 @@
 #include "standard-headers/linux/virtio_blk.h"
 #include "hw/block/block.h"
 #include "hw/virtio/vhost.h"
+#include "qemu/queue.h"
 
 #define TYPE_VHOST_BLK "vhost-blk"
 #define VHOST_BLK(obj) \
@@ -43,6 +44,10 @@ typedef struct VHostBlk {
     struct vhost_dev dev;
     bool vhost_started;
     uint64_t length;
+    uint64_t backend_rdev;
+    QEMUBH *resize_bh;
+    QLIST_ENTRY(VHostBlk) uevent_node;
+    bool uevent_registered;
 } VHostBlk;
 
 #endif
-- 
2.43.5


^ permalink raw reply	[relevance 20%]

* Re: [QEMU HCI-8.0 PATCH 3/5] vhost-blk: add read-only flag
  2026-09-03 12:32 26% ` [QEMU HCI-8.0 PATCH 3/5] vhost-blk: add read-only flag Andrey Zhadchenko
@ 2026-09-03 14:56  6%   ` Andrey Drobyshev
  0 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-03 14:56 UTC (permalink / raw)
  To: Andrey Zhadchenko; +Cc: svt-core, den, andrey.drobyshev

> and set RO respectively. Also compare BLKROGET with the selected

Nit: make commit message start with their own sentence.

> mode and reject r/w if needed.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-143437
> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
>
> diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
> index a3e0010982f..47b6e560d47 100644
> --- a/hw/block/vhost-blk.c
> +++ b/hw/block/vhost-blk.c
> @@ -24,6 +24,7 @@
>  #include "system/system.h"
>  #include "linux-headers/linux/vhost.h"
>  #include <sys/ioctl.h>
> +#include <linux/fs.h>

Previous patch removes the header, now we add it back.  Let's just not
touch it.

>  #include "system/runstate.h"
>  
>  static int vhost_blk_start(VirtIODevice *vdev)
> @@ -233,8 +234,10 @@ static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
>  {
>      BlockConf *conf = &s->conf.conf;
>      struct stat st;
> +    int readonly;
> +    int open_flags = s->conf.readonly ? O_RDONLY : O_RDWR;
>  
> -    s->backend_fd = qemu_open(s->conf.devpath, O_RDWR, errp);
> +    s->backend_fd = qemu_open(s->conf.devpath, open_flags, errp);
>      if (s->backend_fd < 0) {
>          error_prepend(errp, "vhost-blk: unable to open backend: ");
>          return false;
> @@ -252,6 +255,19 @@ static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
>          goto fail;
>      }
>  
> +    if (ioctl(s->backend_fd, BLKROGET, &readonly) < 0) {
> +        error_setg_errno(errp, errno,
> +                         "vhost-blk: unable to get read-only status of '%s'",
> +                         s->conf.devpath);
> +        goto fail;
> +    }
> +
> +    if (readonly && !s->conf.readonly) {
> +        error_setg(errp, "vhost-blk: '%s' is read-only",
> +                   s->conf.devpath);
> +        goto fail;
> +    }
> +

How about doing it similarly to file-posix:

    if (!s->conf.readonly) {
        if (ioctl(s->backend_fd, BLKROGET, &readonly) < 0) {
            error_setg_errno(errp, errno,
                             "vhost-blk: unable to get read-only status of "
                             "'%s'", s->conf.devpath);
            goto fail;
         }

         if (readonly) {
             error_setg_errno(errp, EROFS, "The device is not writable");
             goto fail;
         }
    }

In addition: conf.readonly value comes from libvirt.  conf.devpath is
also provided by libvirt.  Shouldn't we check BLKROGET early on and fail
in libvirt instead of waiting till here?  I'd prefer having both checks.
Leave the check here, but fail early on in libvirt.

Andrey

-- 
Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

^ permalink raw reply	[relevance 6%]

* Re: [QEMU HCI-8.0 PATCH 2/5] vhost-blk: change backend setup
  2026-09-03 12:32 24% ` [QEMU HCI-8.0 PATCH 2/5] vhost-blk: " Andrey Zhadchenko
@ 2026-09-03 14:56  0%   ` Andrey Drobyshev
  2026-09-03 15:27  0%     ` Andrey Zhadchenko
  0 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-09-03 14:56 UTC (permalink / raw)
  To: Andrey Zhadchenko; +Cc: svt-core, den, andrey.drobyshev

> Previously we used very ugly and incapsulation-breaking assignment
> fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
> It is wrong in a many ways, so let's rework this.
> 
> Patch changes default `drive` to new `devpath` option so device fd
> is managed by vhost-blk itself. Unfortunately this way we need a
> bit more preparational work: finding out disk length, block size,
> etc. Don't be too broad and just do the minimal and set reasonable
> default values. Validate with previously introduced
> blkconf_validate_blocksizes().
> Also we lose resize, as this is tied to the block node, which is
> now have no place in the setup. We will add this in the next
> patches as well as RO mode.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-143437
> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
>
> diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
> index c52851fcf8b..a3e0010982f 100644
> --- a/hw/block/vhost-blk.c
> +++ b/hw/block/vhost-blk.c
> @@ -24,23 +24,17 @@
>  #include "system/system.h"
>  #include "linux-headers/linux/vhost.h"
>  #include <sys/ioctl.h>
> -#include <linux/fs.h>
> -#include "include/block/block_int-common.h"
>  #include "system/runstate.h"
>  
>  static int vhost_blk_start(VirtIODevice *vdev)
>  {
>      VHostBlk *s = VHOST_BLK(vdev);
>      struct vhost_vring_file backend;
> -    int ret, i, nworkers, *fd;
> +    int ret, i, nworkers;
>      BusState *qbus = BUS(qdev_get_parent_bus(DEVICE(vdev)));
>      VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
>      char serial[VIRTIO_BLK_ID_BYTES] = {0};
>  
> -    bdrv_graph_rdlock_main_loop();
> -    fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
> -    bdrv_graph_rdunlock_main_loop();
> -
>      if (!k->set_guest_notifiers) {
>          error_report("vhost-blk: binding does not support guest notifiers");
>          return -ENOSYS;
> @@ -92,7 +86,7 @@ static int vhost_blk_start(VirtIODevice *vdev)
>  
>      memset(&backend, 0, sizeof(backend));
>      backend.index = 0;
> -    backend.fd = *fd;
> +    backend.fd = s->backend_fd;
>      if (ioctl(s->vhostfd, VHOST_BLK_SET_BACKEND, &backend)) {
>          error_report("vhost-blk: unable to set backend");
>          ret = -errno;
> @@ -208,29 +202,79 @@ static void vhost_blk_vm_state(void *opaque, bool running, RunState state)
>      }
>  }
>  
> -static void vhost_blk_resize_cb(void *opaque)
> +static int vhost_blk_update_size(VHostBlk *s, Error **errp)
>  {
> -    VirtIODevice *vdev = opaque;
> +    BlockConf *conf = &s->conf.conf;
> +    off_t length;
> +    bool changed;
> +
> +    length = lseek(s->backend_fd, 0, SEEK_END);
> +    if (length < 0) {
> +        int error = errno;
> +
> +        error_setg_errno(errp, error,
> +                         "vhost-blk: unable to determine size of '%s'",
> +                         s->conf.devpath);
> +        return -error;
> +    }
>  
> -    assert(qemu_get_current_aio_context() == qemu_get_aio_context());
> -    virtio_notify_config(vdev);
> +    changed = s->length != length;
> +    s->length = length;
> +    conf->heads = 16;
> +    conf->secs = 63;
> +    conf->cyls = s->length / BDRV_SECTOR_SIZE /
> +                 (conf->heads * conf->secs);
> +    conf->cyls = MIN(MAX(conf->cyls, 2U), 16383U);
> +
> +    return changed;

This function should return int, but here we return bool.  And then
we do 'if (vhost_blk_update_size() < 0) ...', which never fires.

>  }
>  
> -static void vhost_blk_resize(void *opaque)
> +static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
>  {
> -    VirtIODevice *vdev = VIRTIO_DEVICE(opaque);
> +    BlockConf *conf = &s->conf.conf;
> +    struct stat st;
>  
> -    /*
> -     * virtio_notify_config() needs to acquire the global mutex,
> -     * so it can't be called from an iothread. Instead, schedule
> -     * it to be run in the main context BH.
> -     */
> -    aio_bh_schedule_oneshot(qemu_get_aio_context(), vhost_blk_resize_cb, vdev);
> -}
> +    s->backend_fd = qemu_open(s->conf.devpath, O_RDWR, errp);
> +    if (s->backend_fd < 0) {
> +        error_prepend(errp, "vhost-blk: unable to open backend: ");
> +        return false;
> +    }

Suggestion: how about also checking BLKSSZGET value of the device at this
point and comparing it against conf->logical_block_size?

>  
> -static const BlockDevOps vhost_blk_block_ops = {
> -    .resize_cb     = vhost_blk_resize,
> -};
> +    if (fstat(s->backend_fd, &st) < 0) {
> +        error_setg_errno(errp, errno, "vhost-blk: unable to stat '%s'",
> +                         s->conf.devpath);
> +        goto fail;
> +    }
> +
> +    if (!S_ISBLK(st.st_mode)) {
> +        error_setg(errp, "vhost-blk: '%s' is not a block device",
> +                   s->conf.devpath);
> +        goto fail;
> +    }
> +
> +    if (vhost_blk_update_size(s, errp) < 0) {
> +        goto fail;
> +    }
> +
> +    if (!conf->logical_block_size) {
> +        conf->logical_block_size = BDRV_SECTOR_SIZE;
> +    }
> +
> +    if (!conf->physical_block_size) {
> +        conf->physical_block_size = BDRV_SECTOR_SIZE;
> +    }
> +
> +    if (!blkconf_validate_blocksizes(conf, errp)) {
> +        goto fail;
> +    }
> +
> +    return true;
> +
> +fail:
> +    qemu_close(s->backend_fd);
> +    s->backend_fd = -1;
> +    return false;
> +}
>  
>  static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
>  {
> @@ -239,13 +283,8 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
>      VhostBlkConf *conf = &s->conf;
>      int i, ret;
>  
> -    if (!conf->conf.blk) {
> -        error_setg(errp, "vhost-blk: drive property not set");
> -        return;
> -    }
> -
> -    if (!blk_is_inserted(conf->conf.blk)) {
> -        error_setg(errp, "vhost-blk: device needs media, but drive is empty");
> +    if (!conf->devpath) {
> +        error_setg(errp, "vhost-blk: devpath property must be set");
>          return;
>      }
>  
> @@ -273,17 +312,7 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
>          return;
>      }
>  
> -    if (!blkconf_apply_backend_options(&conf->conf,
> -                                       !blk_supports_write_perm(conf->conf.blk),
> -                                       true, errp)) {
> -        return;
> -    }
> -
> -    if (!blkconf_geometry(&conf->conf, NULL, 65535, 255, 255, errp)) {
> -        return;
> -    }
> -
> -    if (!blkconf_blocksizes(&conf->conf, errp)) {
> +    if (!vhost_blk_open_backend(s, errp)) {
>          return;
>      }
>  
> @@ -311,13 +340,13 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
>          goto cleanup;
>      }
>  
> -    blk_set_dev_ops(s->conf.conf.blk, &vhost_blk_block_ops, s);
> -
>      ret = vhost_dev_init(&s->dev, (void *)((size_t)s->vhostfd),
>                           VHOST_BACKEND_TYPE_KERNEL, 0, NULL);
>      if (ret < 0) {
>          error_setg(errp, "vhost-blk: vhost initialization failed: %s",
>                  strerror(-ret));
> +        /* vhost_dev_init() closes vhostfd on failure */
> +        s->vhostfd = -1;

Before this patch we were doing double close(vhostfd) after vhost_dev_init()
failure.  I'd make it a separate commit with a "Fixes:" tag.

>          goto cleanup;
>      }
>  
> @@ -328,7 +357,14 @@ cleanup:
>              qemu_del_vm_change_state_handler(s->mighand);
>      }
>      g_free(s->dev.vqs);
> -    close(s->vhostfd);
> +    if (s->vhostfd >= 0) {
> +        close(s->vhostfd);
> +        s->vhostfd = -1;
> +    }
> +    if (s->backend_fd >= 0) {
> +        qemu_close(s->backend_fd);
> +        s->backend_fd = -1;
> +    }
>      for (i = 0; i < conf->num_queues; i++) {
>          virtio_del_queue(vdev, i);
>      }
> @@ -344,6 +380,10 @@ static void vhost_blk_device_unrealize(DeviceState *dev)
>      qemu_del_vm_change_state_handler(s->mighand);
>      vhost_blk_set_status(vdev, 0);
>      vhost_dev_cleanup(&s->dev);
> +    if (s->backend_fd >= 0) {
> +        qemu_close(s->backend_fd);
> +        s->backend_fd = -1;
> +    }
>      g_free(s->dev.vqs);
>      virtio_cleanup(vdev);
>  }
> @@ -376,10 +416,6 @@ static uint64_t vhost_blk_get_features(VirtIODevice *vdev,
>  
>      virtio_add_feature(&features, VIRTIO_F_VERSION_1);
>  
> -    if (!blk_is_writable(s->conf.conf.blk)) {
> -        virtio_add_feature(&features, VIRTIO_BLK_F_RO);
> -    }
> -
>      if (s->conf.num_queues > 1) {
>          virtio_add_feature(&features, VIRTIO_BLK_F_MQ);
>      }
> @@ -398,7 +434,9 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
>      int64_t length;
>      int blk_size = conf->logical_block_size;
>  
> -    blk_get_geometry(s->conf.conf.blk, &capacity);
> +    length = s->length;
> +    capacity = length / BDRV_SECTOR_SIZE;
> +
>      memset(&blkcfg, 0, sizeof(blkcfg));
>      virtio_stq_p(vdev, &blkcfg.capacity, capacity);
>      virtio_stl_p(vdev, &blkcfg.seg_max, s->conf.queue_size - 2);
> @@ -406,7 +444,6 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
>      virtio_stl_p(vdev, &blkcfg.blk_size, blk_size);
>      blkcfg.geometry.heads = conf->heads;
>  
> -    length = blk_getlength(s->conf.conf.blk);
>      if (length > 0 && length / conf->heads / conf->secs % blk_size) {
>          unsigned short mask;
>  
> @@ -425,7 +462,8 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
>  }
>  
>  static const Property vhost_blk_properties[] = {
> -    DEFINE_BLOCK_PROPERTIES(VHostBlk, conf.conf),
> +    DEFINE_BLOCK_PROPERTIES_BASE(VHostBlk, conf.conf),

DEFINE_BLOCK_PROPERTIES_BASE() macro defines lots of properties that
make no sense without BlockBackend.  E.g. backend_defaults, write-cache,
share-rw, account-invalid, account-failed, stats-intervals.  We should
consider limiting the list of config properties to the ones which really
matter to us.  Ideally as a separate commit.

Andrey

-- 
Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

^ permalink raw reply	[relevance 0%]

* Re: [QEMU HCI-8.0 PATCH 2/5] vhost-blk: change backend setup
  2026-09-03 14:56  0%   ` Andrey Drobyshev
@ 2026-09-03 15:27  0%     ` Andrey Zhadchenko
  2026-09-03 15:34  0%       ` Andrey Drobyshev
  0 siblings, 1 reply; 200+ results
From: Andrey Zhadchenko @ 2026-09-03 15:27 UTC (permalink / raw)
  To: Andrey Drobyshev; +Cc: svt-core, den



On 9/3/26 16:56, Andrey Drobyshev wrote:
>> Previously we used very ugly and incapsulation-breaking assignment
>> fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
>> It is wrong in a many ways, so let's rework this.
>>
>> Patch changes default `drive` to new `devpath` option so device fd
>> is managed by vhost-blk itself. Unfortunately this way we need a
>> bit more preparational work: finding out disk length, block size,
>> etc. Don't be too broad and just do the minimal and set reasonable
>> default values. Validate with previously introduced
>> blkconf_validate_blocksizes().
>> Also we lose resize, as this is tied to the block node, which is
>> now have no place in the setup. We will add this in the next
>> patches as well as RO mode.
>>
>> https://virtuozzo.atlassian.net/browse/VSTOR-143437
>> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
>>
>> diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
>> index c52851fcf8b..a3e0010982f 100644
>> --- a/hw/block/vhost-blk.c
>> +++ b/hw/block/vhost-blk.c
>> @@ -24,23 +24,17 @@
>>   #include "system/system.h"
>>   #include "linux-headers/linux/vhost.h"
>>   #include <sys/ioctl.h>
>> -#include <linux/fs.h>
>> -#include "include/block/block_int-common.h"
>>   #include "system/runstate.h"
>>   
>>   static int vhost_blk_start(VirtIODevice *vdev)
>>   {
>>       VHostBlk *s = VHOST_BLK(vdev);
>>       struct vhost_vring_file backend;
>> -    int ret, i, nworkers, *fd;
>> +    int ret, i, nworkers;
>>       BusState *qbus = BUS(qdev_get_parent_bus(DEVICE(vdev)));
>>       VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
>>       char serial[VIRTIO_BLK_ID_BYTES] = {0};
>>   
>> -    bdrv_graph_rdlock_main_loop();
>> -    fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
>> -    bdrv_graph_rdunlock_main_loop();
>> -
>>       if (!k->set_guest_notifiers) {
>>           error_report("vhost-blk: binding does not support guest notifiers");
>>           return -ENOSYS;
>> @@ -92,7 +86,7 @@ static int vhost_blk_start(VirtIODevice *vdev)
>>   
>>       memset(&backend, 0, sizeof(backend));
>>       backend.index = 0;
>> -    backend.fd = *fd;
>> +    backend.fd = s->backend_fd;
>>       if (ioctl(s->vhostfd, VHOST_BLK_SET_BACKEND, &backend)) {
>>           error_report("vhost-blk: unable to set backend");
>>           ret = -errno;
>> @@ -208,29 +202,79 @@ static void vhost_blk_vm_state(void *opaque, bool running, RunState state)
>>       }
>>   }
>>   
>> -static void vhost_blk_resize_cb(void *opaque)
>> +static int vhost_blk_update_size(VHostBlk *s, Error **errp)
>>   {
>> -    VirtIODevice *vdev = opaque;
>> +    BlockConf *conf = &s->conf.conf;
>> +    off_t length;
>> +    bool changed;
>> +
>> +    length = lseek(s->backend_fd, 0, SEEK_END);
>> +    if (length < 0) {
>> +        int error = errno;
>> +
>> +        error_setg_errno(errp, error,
>> +                         "vhost-blk: unable to determine size of '%s'",
>> +                         s->conf.devpath);
>> +        return -error;
>> +    }
>>   
>> -    assert(qemu_get_current_aio_context() == qemu_get_aio_context());
>> -    virtio_notify_config(vdev);
>> +    changed = s->length != length;
>> +    s->length = length;
>> +    conf->heads = 16;
>> +    conf->secs = 63;
>> +    conf->cyls = s->length / BDRV_SECTOR_SIZE /
>> +                 (conf->heads * conf->secs);
>> +    conf->cyls = MIN(MAX(conf->cyls, 2U), 16383U);
>> +
>> +    return changed;
> 
> This function should return int, but here we return bool.  And then
> we do 'if (vhost_blk_update_size() < 0) ...', which never fires.
> 
>>   }
>>   
>> -static void vhost_blk_resize(void *opaque)
>> +static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
>>   {
>> -    VirtIODevice *vdev = VIRTIO_DEVICE(opaque);
>> +    BlockConf *conf = &s->conf.conf;
>> +    struct stat st;
>>   
>> -    /*
>> -     * virtio_notify_config() needs to acquire the global mutex,
>> -     * so it can't be called from an iothread. Instead, schedule
>> -     * it to be run in the main context BH.
>> -     */
>> -    aio_bh_schedule_oneshot(qemu_get_aio_context(), vhost_blk_resize_cb, vdev);
>> -}
>> +    s->backend_fd = qemu_open(s->conf.devpath, O_RDWR, errp);
>> +    if (s->backend_fd < 0) {
>> +        error_prepend(errp, "vhost-blk: unable to open backend: ");
>> +        return false;
>> +    }
> 
> Suggestion: how about also checking BLKSSZGET value of the device at this
> point and comparing it against conf->logical_block_size?

I would personally avoid this for now. First of all BLKSSZGET (and other 
things) may be undefined (failed this for BLKROGET btw) and proper ifdef
decoration are not so important for 'change backend setup' patch. We can
always add it later.

> 
>>   
>> -static const BlockDevOps vhost_blk_block_ops = {
>> -    .resize_cb     = vhost_blk_resize,
>> -};
>> +    if (fstat(s->backend_fd, &st) < 0) {
>> +        error_setg_errno(errp, errno, "vhost-blk: unable to stat '%s'",
>> +                         s->conf.devpath);
>> +        goto fail;
>> +    }
>> +
>> +    if (!S_ISBLK(st.st_mode)) {
>> +        error_setg(errp, "vhost-blk: '%s' is not a block device",
>> +                   s->conf.devpath);
>> +        goto fail;
>> +    }
>> +
>> +    if (vhost_blk_update_size(s, errp) < 0) {
>> +        goto fail;
>> +    }
>> +
>> +    if (!conf->logical_block_size) {
>> +        conf->logical_block_size = BDRV_SECTOR_SIZE;
>> +    }
>> +
>> +    if (!conf->physical_block_size) {
>> +        conf->physical_block_size = BDRV_SECTOR_SIZE;
>> +    }
>> +
>> +    if (!blkconf_validate_blocksizes(conf, errp)) {
>> +        goto fail;
>> +    }
>> +
>> +    return true;
>> +
>> +fail:
>> +    qemu_close(s->backend_fd);
>> +    s->backend_fd = -1;
>> +    return false;
>> +}
>>   
>>   static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
>>   {
>> @@ -239,13 +283,8 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
>>       VhostBlkConf *conf = &s->conf;
>>       int i, ret;
>>   
>> -    if (!conf->conf.blk) {
>> -        error_setg(errp, "vhost-blk: drive property not set");
>> -        return;
>> -    }
>> -
>> -    if (!blk_is_inserted(conf->conf.blk)) {
>> -        error_setg(errp, "vhost-blk: device needs media, but drive is empty");
>> +    if (!conf->devpath) {
>> +        error_setg(errp, "vhost-blk: devpath property must be set");
>>           return;
>>       }
>>   
>> @@ -273,17 +312,7 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
>>           return;
>>       }
>>   
>> -    if (!blkconf_apply_backend_options(&conf->conf,
>> -                                       !blk_supports_write_perm(conf->conf.blk),
>> -                                       true, errp)) {
>> -        return;
>> -    }
>> -
>> -    if (!blkconf_geometry(&conf->conf, NULL, 65535, 255, 255, errp)) {
>> -        return;
>> -    }
>> -
>> -    if (!blkconf_blocksizes(&conf->conf, errp)) {
>> +    if (!vhost_blk_open_backend(s, errp)) {
>>           return;
>>       }
>>   
>> @@ -311,13 +340,13 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
>>           goto cleanup;
>>       }
>>   
>> -    blk_set_dev_ops(s->conf.conf.blk, &vhost_blk_block_ops, s);
>> -
>>       ret = vhost_dev_init(&s->dev, (void *)((size_t)s->vhostfd),
>>                            VHOST_BACKEND_TYPE_KERNEL, 0, NULL);
>>       if (ret < 0) {
>>           error_setg(errp, "vhost-blk: vhost initialization failed: %s",
>>                   strerror(-ret));
>> +        /* vhost_dev_init() closes vhostfd on failure */
>> +        s->vhostfd = -1;
> 
> Before this patch we were doing double close(vhostfd) after vhost_dev_init()
> failure.  I'd make it a separate commit with a "Fixes:" tag.
> 
>>           goto cleanup;
>>       }
>>   
>> @@ -328,7 +357,14 @@ cleanup:
>>               qemu_del_vm_change_state_handler(s->mighand);
>>       }
>>       g_free(s->dev.vqs);
>> -    close(s->vhostfd);
>> +    if (s->vhostfd >= 0) {
>> +        close(s->vhostfd);
>> +        s->vhostfd = -1;
>> +    }
>> +    if (s->backend_fd >= 0) {
>> +        qemu_close(s->backend_fd);
>> +        s->backend_fd = -1;
>> +    }
>>       for (i = 0; i < conf->num_queues; i++) {
>>           virtio_del_queue(vdev, i);
>>       }
>> @@ -344,6 +380,10 @@ static void vhost_blk_device_unrealize(DeviceState *dev)
>>       qemu_del_vm_change_state_handler(s->mighand);
>>       vhost_blk_set_status(vdev, 0);
>>       vhost_dev_cleanup(&s->dev);
>> +    if (s->backend_fd >= 0) {
>> +        qemu_close(s->backend_fd);
>> +        s->backend_fd = -1;
>> +    }
>>       g_free(s->dev.vqs);
>>       virtio_cleanup(vdev);
>>   }
>> @@ -376,10 +416,6 @@ static uint64_t vhost_blk_get_features(VirtIODevice *vdev,
>>   
>>       virtio_add_feature(&features, VIRTIO_F_VERSION_1);
>>   
>> -    if (!blk_is_writable(s->conf.conf.blk)) {
>> -        virtio_add_feature(&features, VIRTIO_BLK_F_RO);
>> -    }
>> -
>>       if (s->conf.num_queues > 1) {
>>           virtio_add_feature(&features, VIRTIO_BLK_F_MQ);
>>       }
>> @@ -398,7 +434,9 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
>>       int64_t length;
>>       int blk_size = conf->logical_block_size;
>>   
>> -    blk_get_geometry(s->conf.conf.blk, &capacity);
>> +    length = s->length;
>> +    capacity = length / BDRV_SECTOR_SIZE;
>> +
>>       memset(&blkcfg, 0, sizeof(blkcfg));
>>       virtio_stq_p(vdev, &blkcfg.capacity, capacity);
>>       virtio_stl_p(vdev, &blkcfg.seg_max, s->conf.queue_size - 2);
>> @@ -406,7 +444,6 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
>>       virtio_stl_p(vdev, &blkcfg.blk_size, blk_size);
>>       blkcfg.geometry.heads = conf->heads;
>>   
>> -    length = blk_getlength(s->conf.conf.blk);
>>       if (length > 0 && length / conf->heads / conf->secs % blk_size) {
>>           unsigned short mask;
>>   
>> @@ -425,7 +462,8 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
>>   }
>>   
>>   static const Property vhost_blk_properties[] = {
>> -    DEFINE_BLOCK_PROPERTIES(VHostBlk, conf.conf),
>> +    DEFINE_BLOCK_PROPERTIES_BASE(VHostBlk, conf.conf),
> 
> DEFINE_BLOCK_PROPERTIES_BASE() macro defines lots of properties that
> make no sense without BlockBackend.  E.g. backend_defaults, write-cache,
> share-rw, account-invalid, account-failed, stats-intervals.  We should
> consider limiting the list of config properties to the ones which really
> matter to us.  Ideally as a separate commit.

 From one point of view yes, from another point of view a lot other make 
sense for virtio-device. I thought it was better to leave it as is and 
use it later.
But maybe remove it altogether (also along with logical/physical block 
size) and better add it later as separate options if we feel tuning 
these values brings any impact?

> 
> Andrey
> 


^ permalink raw reply	[relevance 0%]

* Re: [QEMU HCI-8.0 PATCH 2/5] vhost-blk: change backend setup
  2026-09-03 15:27  0%     ` Andrey Zhadchenko
@ 2026-09-03 15:34  0%       ` Andrey Drobyshev
  0 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-03 15:34 UTC (permalink / raw)
  To: Andrey Zhadchenko; +Cc: svt-core, den

On 9/3/26 6:27 PM, Andrey Zhadchenko wrote:
> 
> 
> On 9/3/26 16:56, Andrey Drobyshev wrote:
>>> Previously we used very ugly and incapsulation-breaking assignment
>>> fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
>>> It is wrong in a many ways, so let's rework this.
>>>
>>> Patch changes default `drive` to new `devpath` option so device fd
>>> is managed by vhost-blk itself. Unfortunately this way we need a
>>> bit more preparational work: finding out disk length, block size,
>>> etc. Don't be too broad and just do the minimal and set reasonable
>>> default values. Validate with previously introduced
>>> blkconf_validate_blocksizes().
>>> Also we lose resize, as this is tied to the block node, which is
>>> now have no place in the setup. We will add this in the next
>>> patches as well as RO mode.
>>>
>>> https://virtuozzo.atlassian.net/browse/VSTOR-143437
>>> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
>>>
>>> diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
>>> index c52851fcf8b..a3e0010982f 100644
>>> --- a/hw/block/vhost-blk.c
>>> +++ b/hw/block/vhost-blk.c
>>> @@ -24,23 +24,17 @@
>>>   #include "system/system.h"
>>>   #include "linux-headers/linux/vhost.h"
>>>   #include <sys/ioctl.h>
>>> -#include <linux/fs.h>
>>> -#include "include/block/block_int-common.h"
>>>   #include "system/runstate.h"
>>>   
>>>   static int vhost_blk_start(VirtIODevice *vdev)
>>>   {
>>>       VHostBlk *s = VHOST_BLK(vdev);
>>>       struct vhost_vring_file backend;
>>> -    int ret, i, nworkers, *fd;
>>> +    int ret, i, nworkers;
>>>       BusState *qbus = BUS(qdev_get_parent_bus(DEVICE(vdev)));
>>>       VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
>>>       char serial[VIRTIO_BLK_ID_BYTES] = {0};
>>>   
>>> -    bdrv_graph_rdlock_main_loop();
>>> -    fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
>>> -    bdrv_graph_rdunlock_main_loop();
>>> -
>>>       if (!k->set_guest_notifiers) {
>>>           error_report("vhost-blk: binding does not support guest notifiers");
>>>           return -ENOSYS;
>>> @@ -92,7 +86,7 @@ static int vhost_blk_start(VirtIODevice *vdev)
>>>   
>>>       memset(&backend, 0, sizeof(backend));
>>>       backend.index = 0;
>>> -    backend.fd = *fd;
>>> +    backend.fd = s->backend_fd;
>>>       if (ioctl(s->vhostfd, VHOST_BLK_SET_BACKEND, &backend)) {
>>>           error_report("vhost-blk: unable to set backend");
>>>           ret = -errno;
>>> @@ -208,29 +202,79 @@ static void vhost_blk_vm_state(void *opaque, bool running, RunState state)
>>>       }
>>>   }
>>>   
>>> -static void vhost_blk_resize_cb(void *opaque)
>>> +static int vhost_blk_update_size(VHostBlk *s, Error **errp)
>>>   {
>>> -    VirtIODevice *vdev = opaque;
>>> +    BlockConf *conf = &s->conf.conf;
>>> +    off_t length;
>>> +    bool changed;
>>> +
>>> +    length = lseek(s->backend_fd, 0, SEEK_END);
>>> +    if (length < 0) {
>>> +        int error = errno;
>>> +
>>> +        error_setg_errno(errp, error,
>>> +                         "vhost-blk: unable to determine size of '%s'",
>>> +                         s->conf.devpath);
>>> +        return -error;
>>> +    }
>>>   
>>> -    assert(qemu_get_current_aio_context() == qemu_get_aio_context());
>>> -    virtio_notify_config(vdev);
>>> +    changed = s->length != length;
>>> +    s->length = length;
>>> +    conf->heads = 16;
>>> +    conf->secs = 63;
>>> +    conf->cyls = s->length / BDRV_SECTOR_SIZE /
>>> +                 (conf->heads * conf->secs);
>>> +    conf->cyls = MIN(MAX(conf->cyls, 2U), 16383U);
>>> +
>>> +    return changed;
>>
>> This function should return int, but here we return bool.  And then
>> we do 'if (vhost_blk_update_size() < 0) ...', which never fires.
>>
>>>   }
>>>   
>>> -static void vhost_blk_resize(void *opaque)
>>> +static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
>>>   {
>>> -    VirtIODevice *vdev = VIRTIO_DEVICE(opaque);
>>> +    BlockConf *conf = &s->conf.conf;
>>> +    struct stat st;
>>>   
>>> -    /*
>>> -     * virtio_notify_config() needs to acquire the global mutex,
>>> -     * so it can't be called from an iothread. Instead, schedule
>>> -     * it to be run in the main context BH.
>>> -     */
>>> -    aio_bh_schedule_oneshot(qemu_get_aio_context(), vhost_blk_resize_cb, vdev);
>>> -}
>>> +    s->backend_fd = qemu_open(s->conf.devpath, O_RDWR, errp);
>>> +    if (s->backend_fd < 0) {
>>> +        error_prepend(errp, "vhost-blk: unable to open backend: ");
>>> +        return false;
>>> +    }
>>
>> Suggestion: how about also checking BLKSSZGET value of the device at this
>> point and comparing it against conf->logical_block_size?
> 
> I would personally avoid this for now. First of all BLKSSZGET (and other 
> things) may be undefined (failed this for BLKROGET btw) and proper ifdef
> decoration are not so important for 'change backend setup' patch. We can
> always add it later.
>

At least in our headers BLKROGET and BLKSSZGET are defined together.
Both conf.readonly and conf.logical_block_size are set by the user (i.e.
libvirt) and might mismatch with the actual device state.  So IMHO
they're symmetrical in this regard.

>>
>>>   
>>> -static const BlockDevOps vhost_blk_block_ops = {
>>> -    .resize_cb     = vhost_blk_resize,
>>> -};
>>> +    if (fstat(s->backend_fd, &st) < 0) {
>>> +        error_setg_errno(errp, errno, "vhost-blk: unable to stat '%s'",
>>> +                         s->conf.devpath);
>>> +        goto fail;
>>> +    }
>>> +
>>> +    if (!S_ISBLK(st.st_mode)) {
>>> +        error_setg(errp, "vhost-blk: '%s' is not a block device",
>>> +                   s->conf.devpath);
>>> +        goto fail;
>>> +    }
>>> +
>>> +    if (vhost_blk_update_size(s, errp) < 0) {
>>> +        goto fail;
>>> +    }
>>> +
>>> +    if (!conf->logical_block_size) {
>>> +        conf->logical_block_size = BDRV_SECTOR_SIZE;
>>> +    }
>>> +
>>> +    if (!conf->physical_block_size) {
>>> +        conf->physical_block_size = BDRV_SECTOR_SIZE;
>>> +    }
>>> +
>>> +    if (!blkconf_validate_blocksizes(conf, errp)) {
>>> +        goto fail;
>>> +    }
>>> +
>>> +    return true;
>>> +
>>> +fail:
>>> +    qemu_close(s->backend_fd);
>>> +    s->backend_fd = -1;
>>> +    return false;
>>> +}
>>>   
>>>   static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
>>>   {
>>> @@ -239,13 +283,8 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
>>>       VhostBlkConf *conf = &s->conf;
>>>       int i, ret;
>>>   
>>> -    if (!conf->conf.blk) {
>>> -        error_setg(errp, "vhost-blk: drive property not set");
>>> -        return;
>>> -    }
>>> -
>>> -    if (!blk_is_inserted(conf->conf.blk)) {
>>> -        error_setg(errp, "vhost-blk: device needs media, but drive is empty");
>>> +    if (!conf->devpath) {
>>> +        error_setg(errp, "vhost-blk: devpath property must be set");
>>>           return;
>>>       }
>>>   
>>> @@ -273,17 +312,7 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
>>>           return;
>>>       }
>>>   
>>> -    if (!blkconf_apply_backend_options(&conf->conf,
>>> -                                       !blk_supports_write_perm(conf->conf.blk),
>>> -                                       true, errp)) {
>>> -        return;
>>> -    }
>>> -
>>> -    if (!blkconf_geometry(&conf->conf, NULL, 65535, 255, 255, errp)) {
>>> -        return;
>>> -    }
>>> -
>>> -    if (!blkconf_blocksizes(&conf->conf, errp)) {
>>> +    if (!vhost_blk_open_backend(s, errp)) {
>>>           return;
>>>       }
>>>   
>>> @@ -311,13 +340,13 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
>>>           goto cleanup;
>>>       }
>>>   
>>> -    blk_set_dev_ops(s->conf.conf.blk, &vhost_blk_block_ops, s);
>>> -
>>>       ret = vhost_dev_init(&s->dev, (void *)((size_t)s->vhostfd),
>>>                            VHOST_BACKEND_TYPE_KERNEL, 0, NULL);
>>>       if (ret < 0) {
>>>           error_setg(errp, "vhost-blk: vhost initialization failed: %s",
>>>                   strerror(-ret));
>>> +        /* vhost_dev_init() closes vhostfd on failure */
>>> +        s->vhostfd = -1;
>>
>> Before this patch we were doing double close(vhostfd) after vhost_dev_init()
>> failure.  I'd make it a separate commit with a "Fixes:" tag.
>>
>>>           goto cleanup;
>>>       }
>>>   
>>> @@ -328,7 +357,14 @@ cleanup:
>>>               qemu_del_vm_change_state_handler(s->mighand);
>>>       }
>>>       g_free(s->dev.vqs);
>>> -    close(s->vhostfd);
>>> +    if (s->vhostfd >= 0) {
>>> +        close(s->vhostfd);
>>> +        s->vhostfd = -1;
>>> +    }
>>> +    if (s->backend_fd >= 0) {
>>> +        qemu_close(s->backend_fd);
>>> +        s->backend_fd = -1;
>>> +    }
>>>       for (i = 0; i < conf->num_queues; i++) {
>>>           virtio_del_queue(vdev, i);
>>>       }
>>> @@ -344,6 +380,10 @@ static void vhost_blk_device_unrealize(DeviceState *dev)
>>>       qemu_del_vm_change_state_handler(s->mighand);
>>>       vhost_blk_set_status(vdev, 0);
>>>       vhost_dev_cleanup(&s->dev);
>>> +    if (s->backend_fd >= 0) {
>>> +        qemu_close(s->backend_fd);
>>> +        s->backend_fd = -1;
>>> +    }
>>>       g_free(s->dev.vqs);
>>>       virtio_cleanup(vdev);
>>>   }
>>> @@ -376,10 +416,6 @@ static uint64_t vhost_blk_get_features(VirtIODevice *vdev,
>>>   
>>>       virtio_add_feature(&features, VIRTIO_F_VERSION_1);
>>>   
>>> -    if (!blk_is_writable(s->conf.conf.blk)) {
>>> -        virtio_add_feature(&features, VIRTIO_BLK_F_RO);
>>> -    }
>>> -
>>>       if (s->conf.num_queues > 1) {
>>>           virtio_add_feature(&features, VIRTIO_BLK_F_MQ);
>>>       }
>>> @@ -398,7 +434,9 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
>>>       int64_t length;
>>>       int blk_size = conf->logical_block_size;
>>>   
>>> -    blk_get_geometry(s->conf.conf.blk, &capacity);
>>> +    length = s->length;
>>> +    capacity = length / BDRV_SECTOR_SIZE;
>>> +
>>>       memset(&blkcfg, 0, sizeof(blkcfg));
>>>       virtio_stq_p(vdev, &blkcfg.capacity, capacity);
>>>       virtio_stl_p(vdev, &blkcfg.seg_max, s->conf.queue_size - 2);
>>> @@ -406,7 +444,6 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
>>>       virtio_stl_p(vdev, &blkcfg.blk_size, blk_size);
>>>       blkcfg.geometry.heads = conf->heads;
>>>   
>>> -    length = blk_getlength(s->conf.conf.blk);
>>>       if (length > 0 && length / conf->heads / conf->secs % blk_size) {
>>>           unsigned short mask;
>>>   
>>> @@ -425,7 +462,8 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
>>>   }
>>>   
>>>   static const Property vhost_blk_properties[] = {
>>> -    DEFINE_BLOCK_PROPERTIES(VHostBlk, conf.conf),
>>> +    DEFINE_BLOCK_PROPERTIES_BASE(VHostBlk, conf.conf),
>>
>> DEFINE_BLOCK_PROPERTIES_BASE() macro defines lots of properties that
>> make no sense without BlockBackend.  E.g. backend_defaults, write-cache,
>> share-rw, account-invalid, account-failed, stats-intervals.  We should
>> consider limiting the list of config properties to the ones which really
>> matter to us.  Ideally as a separate commit.
> 
>  From one point of view yes, from another point of view a lot other make 
> sense for virtio-device. I thought it was better to leave it as is and 
> use it later.
> But maybe remove it altogether (also along with logical/physical block 
> size) and better add it later as separate options if we feel tuning 
> these values brings any impact?


In general I'd just vote for limiting that list to the properties that
we're actually using and that matter to us.  Whether it's done via a new
list or via limiting existing ones is technical details.

Andrey

^ permalink raw reply	[relevance 0%]

* [QEMU HCI-8.0 PATCH 4/7] hw/display/qxl: fix TOCTOU in cursor chunk data_size handling #VSTOR-144000
    2026-09-03 20:25  8% ` [QEMU HCI-8.0 PATCH 3/7] hw/display/qxl: Fix mono cursor validation that can read past a cursor chunk #VSTOR-144000 Denis V. Lunev
@ 2026-09-03 20:25  3% ` Denis V. Lunev
  2026-09-03 20:25  7% ` [QEMU HCI-8.0 PATCH 7/7] hw/display/qxl: validate primary surface stride against width #VSTOR-144000 Denis V. Lunev
  2 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-03 20:25 UTC (permalink / raw)
  To: svt-core; +Cc: den

From: Marc-André Lureau <marcandre.lureau@redhat.com>

Snapshot chunk.data_size into a host-local variable before passing it to
qxl_phys2virt() for validation, and pass it through qxl_cursor() and
qxl_unpack_chunks() so that no subsequent code re-reads the field.

Without this, a racing vCPU can inflate data_size between the
qxl_phys2virt() validation and the memcpy in qxl_unpack_chunks(),
causing a source read past the validated region. In practice the read
stays within the guest's own VRAM mmap, so the impact is limited.

Resolves: https://gitlab.com/qemu-project/qemu/-/work_items/3757
Reported-by: Feifan Qian <bea1e@proton.me>
Signed-off-by: Marc-Andre Lureau <marcandre.lureau@redhat.com>
Reviewed-by: Philippe Mathieu-Daudé <philmd@oss.qualcomm.com>
Message-ID: <20260710134352.2313675-1-marcandre.lureau@redhat.com>
Signed-off-by: Philippe Mathieu-Daudé <philmd@oss.qualcomm.com>
(cherry picked from commit a3cc0069e151e5eb5db57bb4e86b00861d5b97ab)
---
 hw/display/qxl-render.c | 28 +++++++++++++++++-----------
 1 file changed, 17 insertions(+), 11 deletions(-)

diff --git a/hw/display/qxl-render.c b/hw/display/qxl-render.c
index 1fe63b6f5ca..cf0f6849cd0 100644
--- a/hw/display/qxl-render.c
+++ b/hw/display/qxl-render.c
@@ -217,7 +217,8 @@ void qxl_render_update_area_done(PCIQXLDevice *qxl, QXLCookie *cookie)
 }
 
 static void qxl_unpack_chunks(void *dest, size_t size, PCIQXLDevice *qxl,
-                              QXLDataChunk *chunk, uint32_t group_id)
+                              QXLDataChunk *chunk, uint32_t group_id,
+                              uint32_t chunk_data_size)
 {
     uint32_t max_chunks = 32;
     size_t offset = 0;
@@ -225,22 +226,21 @@ static void qxl_unpack_chunks(void *dest, size_t size, PCIQXLDevice *qxl,
     QXLPHYSICAL next_chunk_phys = 0;
 
     for (;;) {
-        bytes = MIN(size - offset, chunk->data_size);
+        bytes = MIN(size - offset, chunk_data_size);
         memcpy(dest + offset, chunk->data, bytes);
         offset += bytes;
         if (offset == size) {
             return;
         }
         next_chunk_phys = chunk->next_chunk;
-        /* fist time, only get the next chunk's data size */
         chunk = qxl_phys2virt(qxl, next_chunk_phys, group_id,
                               sizeof(QXLDataChunk));
         if (!chunk) {
             return;
         }
-        /* second time, check data size and get data */
+        chunk_data_size = chunk->data_size;
         chunk = qxl_phys2virt(qxl, next_chunk_phys, group_id,
-                              sizeof(QXLDataChunk) + chunk->data_size);
+                              sizeof(QXLDataChunk) + chunk_data_size);
         if (!chunk) {
             return;
         }
@@ -252,7 +252,7 @@ static void qxl_unpack_chunks(void *dest, size_t size, PCIQXLDevice *qxl,
 }
 
 static QEMUCursor *qxl_cursor(PCIQXLDevice *qxl, QXLCursor *cursor,
-                              uint32_t group_id)
+                              uint32_t group_id, uint32_t chunk_data_size)
 {
     QEMUCursor *c;
     uint8_t *and_mask, *xor_mask;
@@ -272,11 +272,11 @@ static QEMUCursor *qxl_cursor(PCIQXLDevice *qxl, QXLCursor *cursor,
     case SPICE_CURSOR_TYPE_MONO:
         /* Assume that the full cursor is available in a single chunk. */
         size = 2 * cursor_get_mono_bpl(c) * c->height;
-        if (size != cursor->data_size || cursor->chunk.data_size < size) {
+        if (size != cursor->data_size || chunk_data_size < size) {
             qxl_set_guest_bug(qxl, "%s: bad monochrome cursor %ux%u"
                               " data_size %u chunk_size %u",
                               __func__, c->width, c->height,
-                              cursor->data_size, cursor->chunk.data_size);
+                              cursor->data_size, chunk_data_size);
             goto fail;
         }
         and_mask = cursor->chunk.data;
@@ -288,7 +288,8 @@ static QEMUCursor *qxl_cursor(PCIQXLDevice *qxl, QXLCursor *cursor,
         break;
     case SPICE_CURSOR_TYPE_ALPHA:
         size = sizeof(uint32_t) * c->width * c->height;
-        qxl_unpack_chunks(c->data, size, qxl, &cursor->chunk, group_id);
+        qxl_unpack_chunks(c->data, size, qxl, &cursor->chunk, group_id,
+                          chunk_data_size);
         if (qxl->debug > 2) {
             cursor_print_ascii_art(c, "qxl/alpha");
         }
@@ -325,19 +326,23 @@ int qxl_render_cursor(PCIQXLDevice *qxl, QXLCommandExt *ext)
     }
     switch (cmd->type) {
     case QXL_CURSOR_SET:
+    {
+        uint32_t chunk_data_size;
+
         /* First read the QXLCursor to get QXLDataChunk::data_size ... */
         cursor = qxl_phys2virt(qxl, cmd->u.set.shape, ext->group_id,
                                sizeof(QXLCursor));
         if (!cursor) {
             return 1;
         }
+        chunk_data_size = cursor->chunk.data_size;
         /* Then read including the chunked data following QXLCursor. */
         cursor = qxl_phys2virt(qxl, cmd->u.set.shape, ext->group_id,
-                               sizeof(QXLCursor) + cursor->chunk.data_size);
+                               sizeof(QXLCursor) + chunk_data_size);
         if (!cursor) {
             return 1;
         }
-        c = qxl_cursor(qxl, cursor, ext->group_id);
+        c = qxl_cursor(qxl, cursor, ext->group_id, chunk_data_size);
         if (c == NULL) {
             c = cursor_builtin_left_ptr();
         }
@@ -351,6 +356,7 @@ int qxl_render_cursor(PCIQXLDevice *qxl, QXLCommandExt *ext)
         qemu_mutex_unlock(&qxl->ssd.lock);
         qemu_bh_schedule(qxl->ssd.cursor_bh);
         break;
+    }
     case QXL_CURSOR_MOVE:
         qemu_mutex_lock(&qxl->ssd.lock);
         qxl->ssd.mouse_x = cmd->u.position.x;
-- 
2.53.0


^ permalink raw reply	[relevance 3%]

* [QEMU HCI-8.0 PATCH 3/7] hw/display/qxl: Fix mono cursor validation that can read past a cursor chunk #VSTOR-144000
  @ 2026-09-03 20:25  8% ` Denis V. Lunev
  2026-09-03 20:25  3% ` [QEMU HCI-8.0 PATCH 4/7] hw/display/qxl: fix TOCTOU in cursor chunk data_size handling #VSTOR-144000 Denis V. Lunev
  2026-09-03 20:25  7% ` [QEMU HCI-8.0 PATCH 7/7] hw/display/qxl: validate primary surface stride against width #VSTOR-144000 Denis V. Lunev
  2 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-03 20:25 UTC (permalink / raw)
  To: svt-core; +Cc: den

From: Thomas Huth <thuth@redhat.com>

qxl_render_cursor() maps the guest-provided QXLCursor object using the
guest-controlled cursor->chunk.data_size.
For a mono cursor, qxl_cursor() then validates the expected bitmap size
against cursor->data_size, but it does not validate that the first chunk
actually contains that many bytes.
A guest could set cursor->data_size to the correct full mono cursor size
while setting cursor->chunk.data_size to zero. In that case, cursor_set_mono()
reads the AND/XOR masks starting at cursor->chunk.data. If the cursor object
is placed at the end of the QXL RAM BAR, those reads cross the mapped RAM
region and could crash the QEMU process (e.g. under ASan).

Fix it by double-checking cursor->chunk.data_size for the correct size.

This patch is based on the suggested changes by the reporter in the bug
ticket.

Reported-by: huntr bubble
Resolves: https://gitlab.com/qemu-project/qemu/-/work_items/3646
Signed-off-by: Thomas Huth <thuth@redhat.com>
Acked-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Message-ID: <20260630101022.379057-1-thuth@redhat.com>
Signed-off-by: Philippe Mathieu-Daudé <philmd@oss.qualcomm.com>
(cherry picked from commit 0e51b71c7b7706923536c1f7923cace82877932b)
---
 hw/display/qxl-render.c | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/hw/display/qxl-render.c b/hw/display/qxl-render.c
index c6a9ac1da10..1fe63b6f5ca 100644
--- a/hw/display/qxl-render.c
+++ b/hw/display/qxl-render.c
@@ -272,9 +272,11 @@ static QEMUCursor *qxl_cursor(PCIQXLDevice *qxl, QXLCursor *cursor,
     case SPICE_CURSOR_TYPE_MONO:
         /* Assume that the full cursor is available in a single chunk. */
         size = 2 * cursor_get_mono_bpl(c) * c->height;
-        if (size != cursor->data_size) {
-            fprintf(stderr, "%s: bad monochrome cursor %ux%u with size %u\n",
-                    __func__, c->width, c->height, cursor->data_size);
+        if (size != cursor->data_size || cursor->chunk.data_size < size) {
+            qxl_set_guest_bug(qxl, "%s: bad monochrome cursor %ux%u"
+                              " data_size %u chunk_size %u",
+                              __func__, c->width, c->height,
+                              cursor->data_size, cursor->chunk.data_size);
             goto fail;
         }
         and_mask = cursor->chunk.data;
-- 
2.53.0


^ permalink raw reply	[relevance 8%]

* [QEMU HCI-8.0 PATCH 7/7] hw/display/qxl: validate primary surface stride against width #VSTOR-144000
    2026-09-03 20:25  8% ` [QEMU HCI-8.0 PATCH 3/7] hw/display/qxl: Fix mono cursor validation that can read past a cursor chunk #VSTOR-144000 Denis V. Lunev
  2026-09-03 20:25  3% ` [QEMU HCI-8.0 PATCH 4/7] hw/display/qxl: fix TOCTOU in cursor chunk data_size handling #VSTOR-144000 Denis V. Lunev
@ 2026-09-03 20:25  7% ` Denis V. Lunev
  2 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-03 20:25 UTC (permalink / raw)
  To: svt-core; +Cc: den

From: Marc-André Lureau <marcandre.lureau@redhat.com>

The existing validation in qxl_create_guest_primary() checks that
abs(stride) * height fits in vgamem_size and that stride is 4-byte
aligned, but never checks that abs(stride) is large enough to hold one
row of pixels for the declared width and format.

A malicious guest can create a primary surface with a stride much
smaller than width * bytes_per_pixel (e.g. stride=4 for a 64-wide 32bpp
surface). The spice server rejects this via red_validate_surface(), but
the return is void and QEMU unconditionally proceeds to set up the local
rendering state. On the next display refresh, VNC or SDL reads width *
bytes_pp per scanline from a region backed by only stride bytes per
row, causing a host-side out-of-bounds read.

Add three checks in qxl_create_guest_primary() before creating the
surface:
 - reject unknown surface formats
 - reject zero width or height
 - reject surfaces where abs(stride) < width * bytes_per_pixel

Also fix three related issues in qxl-render.c:
 - qxl_blit() used abs_stride to advance the dst pointer into the
   DisplaySurface, but when stride is negative the DisplaySurface is a
   packed buffer whose stride may be smaller. Use surface_stride()
   instead.
 - qxl_render_update_area_unlocked() uses guest_head0_width (set via
   QXL_IO_MONITORS_CONFIG_ASYNC) without validating it against
   abs_stride, bypassing the new validation. Clamp the effective width
   to abs_stride / bytes_pp to prevent out-of-bounds access while
   tolerating the normal transient where the monitor config arrives
   before the primary surface is resized to match.
 - Similarly, guest_head0_height bypasses qxl_create_guest_primary()
   validation. Without clamping, abs_stride * height can overrun
   vgamem_size, and the product can also overflow 32 bits (e.g.
   abs_stride=16 MiB, height=256 wraps to zero), defeating the
   qxl_phys2virt() bounds check. Clamp height to
   vgamem_size / abs_stride to prevent both.

While touch it, fix some endianness issues.

Fixes: CVE-2026-16271
Fixes: a19cbfb34642 ("spice: add qxl device")
Fixes: 979f7ef8966b ("qxl: use guest_monitor_config for local renderer.")
Resolves: https://gitlab.com/qemu-project/qemu/-/work_items/3637
Reported-by: huntr bubble
Signed-off-by: Marc-Andre Lureau <marcandre.lureau@redhat.com>
Reviewed-by: Akihiko Odaki <odaki@rsg.ci.i.u-tokyo.ac.jp>
Message-ID: <20260806094028.640676-1-marcandre.lureau@redhat.com>
(cherry picked from commit ab7183ed4eecb4727532e3ffe5953d127e102c72)
---
 hw/display/qxl-render.c | 66 +++++++++++++++++++++++++----------------
 hw/display/qxl.c        | 59 ++++++++++++++++++++++++++++++++++++
 hw/display/qxl.h        |  2 ++
 3 files changed, 101 insertions(+), 26 deletions(-)

diff --git a/hw/display/qxl-render.c b/hw/display/qxl-render.c
index cf0f6849cd0..f2bc25f03ad 100644
--- a/hw/display/qxl-render.c
+++ b/hw/display/qxl-render.c
@@ -27,6 +27,7 @@
 static void qxl_blit(PCIQXLDevice *qxl, QXLRect *rect)
 {
     DisplaySurface *surface = qemu_console_surface(qxl->vga.con);
+    int dst_stride = surface_stride(surface);
     uint8_t *dst = surface_data(surface);
     uint8_t *src;
     int len, i;
@@ -45,14 +46,14 @@ static void qxl_blit(PCIQXLDevice *qxl, QXLRect *rect)
     } else {
         src += rect->top * qxl->guest_primary.abs_stride;
     }
-    dst += rect->top  * qxl->guest_primary.abs_stride;
+    dst += rect->top  * dst_stride;
     src += rect->left * qxl->guest_primary.bytes_pp;
     dst += rect->left * qxl->guest_primary.bytes_pp;
     len  = (rect->right - rect->left) * qxl->guest_primary.bytes_pp;
 
     for (i = rect->top; i < rect->bottom; i++) {
         memcpy(dst, src, len);
-        dst += qxl->guest_primary.abs_stride;
+        dst += dst_stride;
         src += qxl->guest_primary.qxl_stride;
     }
 }
@@ -61,30 +62,13 @@ void qxl_render_resize(PCIQXLDevice *qxl)
 {
     QXLSurfaceCreate *sc = &qxl->guest_primary.surface;
 
-    qxl->guest_primary.qxl_stride = sc->stride;
-    qxl->guest_primary.abs_stride = abs(sc->stride);
+    qxl->guest_primary.qxl_stride = le32_to_cpu(sc->stride);
+    qxl->guest_primary.abs_stride = abs(qxl->guest_primary.qxl_stride);
     qxl->guest_primary.resized++;
-    switch (sc->format) {
-    case SPICE_SURFACE_FMT_16_555:
-        qxl->guest_primary.bytes_pp = 2;
-        qxl->guest_primary.bits_pp = 15;
-        break;
-    case SPICE_SURFACE_FMT_16_565:
-        qxl->guest_primary.bytes_pp = 2;
-        qxl->guest_primary.bits_pp = 16;
-        break;
-    case SPICE_SURFACE_FMT_32_xRGB:
-    case SPICE_SURFACE_FMT_32_ARGB:
-        qxl->guest_primary.bytes_pp = 4;
-        qxl->guest_primary.bits_pp = 32;
-        break;
-    default:
-        fprintf(stderr, "%s: unhandled format: %x\n", __func__,
-                qxl->guest_primary.surface.format);
-        qxl->guest_primary.bytes_pp = 4;
-        qxl->guest_primary.bits_pp = 32;
-        break;
-    }
+    /* fallback to default bpp if format is unknown */
+    qxl_format_bpp(qxl, le32_to_cpu(sc->format),
+                   &qxl->guest_primary.bytes_pp,
+                   &qxl->guest_primary.bits_pp);
 }
 
 static void qxl_set_rect_to_surface(PCIQXLDevice *qxl, QXLRect *area)
@@ -101,15 +85,45 @@ static void qxl_render_update_area_unlocked(PCIQXLDevice *qxl)
     DisplaySurface *surface;
     int width = qxl->guest_head0_width ?: qxl->guest_primary.surface.width;
     int height = qxl->guest_head0_height ?: qxl->guest_primary.surface.height;
+    uint64_t map_height;
     int i;
 
+    if (width <= 0 || height <= 0) {
+        goto end;
+    }
+
+    if (qxl->guest_primary.bytes_pp > 0) {
+        int max_width = qxl->guest_primary.abs_stride
+                        / qxl->guest_primary.bytes_pp;
+        width = MIN(width, max_width);
+    }
+
+    if (qxl->guest_primary.qxl_stride < 0) {
+        /* qxl_blit() uses the primary height to find the first scanline. */
+        height = MIN(height, (int)qxl->guest_primary.surface.height);
+    }
+
+    if (qxl->guest_primary.abs_stride > 0) {
+        int max_height = qxl->vgamem_size / qxl->guest_primary.abs_stride;
+        height = MIN(height, max_height);
+    }
+
+    /*
+     * height limits the visible update, while map_height is the guest memory
+     * span validated by qxl_phys2virt().  With a negative stride qxl_blit()
+     * addresses scanlines from the declared primary height, so a shorter
+     * monitor still requires validating the full primary surface.
+     */
+    map_height = qxl->guest_primary.qxl_stride < 0 ?
+                 qxl->guest_primary.surface.height : height;
+
     if (qxl->guest_primary.resized) {
         qxl->guest_primary.resized = 0;
         qxl->guest_primary.data = qxl_phys2virt(qxl,
                                                 qxl->guest_primary.surface.mem,
                                                 MEMSLOT_GROUP_GUEST,
                                                 qxl->guest_primary.abs_stride
-                                                * height);
+                                                * map_height);
         if (!qxl->guest_primary.data) {
             goto end;
         }
diff --git a/hw/display/qxl.c b/hw/display/qxl.c
index 994bfcaa522..f0cf346c430 100644
--- a/hw/display/qxl.c
+++ b/hw/display/qxl.c
@@ -1509,6 +1509,47 @@ static void qxl_create_guest_primary_complete(PCIQXLDevice *qxl)
     qxl_render_resize(qxl);
 }
 
+/*
+ * Convert a SpiceSurfaceFormat to bytes per pixel and bits per pixel.
+ *
+ * Only valid for surface suitable for rendering.
+ */
+bool qxl_format_bpp(PCIQXLDevice *qxl, SpiceSurfaceFmt format,
+                    uint32_t *bytes_pp, uint32_t *bits_pp)
+{
+    uint32_t bypp = 4;
+    uint32_t bipp = 32;
+    bool ret = true;
+
+    switch (format) {
+    case SPICE_SURFACE_FMT_16_555:
+        bypp = 2;
+        bipp = 15;
+        break;
+    case SPICE_SURFACE_FMT_16_565:
+        bypp = 2;
+        bipp = 16;
+        break;
+    case SPICE_SURFACE_FMT_32_xRGB:
+    case SPICE_SURFACE_FMT_32_ARGB:
+        bypp = 4;
+        bipp = 32;
+        break;
+    default:
+        ret = false;
+        qxl_set_guest_bug(qxl, "%s: unhandled format: %x", __func__, format);
+    }
+
+    if (bytes_pp != NULL) {
+        *bytes_pp = bypp;
+    }
+    if (bits_pp != NULL) {
+        *bits_pp = bipp;
+    }
+
+    return ret;
+}
+
 static void qxl_create_guest_primary(PCIQXLDevice *qxl, int loadvm,
                                      qxl_async_io async)
 {
@@ -1516,6 +1557,7 @@ static void qxl_create_guest_primary(PCIQXLDevice *qxl, int loadvm,
     QXLSurfaceCreate *sc = &qxl->guest_primary.surface;
     uint32_t requested_height = le32_to_cpu(sc->height);
     int requested_stride = le32_to_cpu(sc->stride);
+    uint32_t bytes_pp;
 
     if (requested_stride == INT32_MIN ||
         abs(requested_stride) * (uint64_t)requested_height
@@ -1552,6 +1594,23 @@ static void qxl_create_guest_primary(PCIQXLDevice *qxl, int loadvm,
         return;
     }
 
+    if (!qxl_format_bpp(qxl, surface.format, &bytes_pp, NULL)) {
+        return;
+    }
+
+    if (surface.width == 0 || surface.height == 0) {
+        qxl_set_guest_bug(qxl, "%s: zero dimension %ux%u",
+                          __func__, surface.width, surface.height);
+        return;
+    }
+
+    if ((uint64_t)surface.width * bytes_pp > abs(surface.stride)) {
+        qxl_set_guest_bug(qxl, "%s: stride too small for width:"
+                          " stride %d width %u bpp %u",
+                          __func__, surface.stride, surface.width, bytes_pp);
+        return;
+    }
+
     surface.mouse_mode = true;
     surface.group_id   = MEMSLOT_GROUP_GUEST;
     if (loadvm) {
diff --git a/hw/display/qxl.h b/hw/display/qxl.h
index a25d9865453..ed5f71c0a3a 100644
--- a/hw/display/qxl.h
+++ b/hw/display/qxl.h
@@ -182,6 +182,8 @@ void qxl_spice_oom(PCIQXLDevice *qxl);
 void qxl_spice_reset_memslots(PCIQXLDevice *qxl);
 void qxl_spice_reset_image_cache(PCIQXLDevice *qxl);
 void qxl_spice_reset_cursor(PCIQXLDevice *qxl);
+bool qxl_format_bpp(PCIQXLDevice *qxl, SpiceSurfaceFmt format,
+                    uint32_t *bytes_pp, uint32_t *bits_pp);
 
 /* qxl-logger.c */
 int qxl_log_cmd_cursor(PCIQXLDevice *qxl, QXLCursorCmd *cmd, int group_id);
-- 
2.53.0


^ permalink raw reply	[relevance 7%]

* [QEMU HCI-8.0 PATCH v2 5/5] vhost-blk: preserve the uevent socket across cpr-exec
                     ` (3 preceding siblings ...)
  2026-09-04 13:21 20% ` [QEMU HCI-8.0 PATCH v2 4/5] vhost-blk: watch the device for resize events Andrey Zhadchenko
@ 2026-09-04 13:21 27% ` Andrey Zhadchenko
  2026-09-04 15:33  0%   ` Andrey Drobyshev
  4 siblings, 1 reply; 200+ results
From: Andrey Zhadchenko @ 2026-09-04 13:21 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

qemu-update uses cpr-exec migration: QEMU re-execs itself in place.
A plain monitor fdset descriptor like our uevent socket does not
survive that (unlike the cpr_save_fd()'d tap/vhost fds), so without
help the resized-device notifications would silently stop until the
next full VM start.

Preserve it the same way the net backends do: on cold boot
cpr_save_fd() the socket under the device's canonical path, and on
the re-exec'd (incoming) side adopt it with cpr_find_fd() instead of
reopening a command line fd that no longer resolves. The management
layer passes a placeholder for the "ueventfd" property in that case.
Drop the saved descriptor on teardown so it does not outlive the
device.

https://virtuozzo.atlassian.net/browse/VSTOR-143437
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
 hw/block/vhost-blk.c | 39 ++++++++++++++++++++++++++++++---------
 1 file changed, 30 insertions(+), 9 deletions(-)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index 7cb842a859..eca12356ae 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 "migration/cpr.h"
 #include "system/runstate.h"
 
 static int vhost_blk_start(VirtIODevice *vdev)
@@ -364,20 +365,36 @@ static bool vhost_blk_uevent_check(int fd, const char *src, Error **errp)
 
 static bool vhost_blk_uevent_attach(VHostBlk *s, Error **errp)
 {
+    g_autofree char *cpr_name = NULL;
+
     if (!s->conf.ueventfd) {
         return true;
     }
 
-    s->uevent_fd = qemu_open(s->conf.ueventfd, O_RDWR, errp);
-    if (s->uevent_fd < 0) {
-        error_prepend(errp, "vhost-blk: unable to open uevent socket: ");
-        return false;
-    }
+    cpr_name = object_get_canonical_path(OBJECT(s));
 
-    if (!vhost_blk_uevent_check(s->uevent_fd, s->conf.ueventfd, errp)) {
-        qemu_close(s->uevent_fd);
-        s->uevent_fd = -1;
-        return false;
+    if (cpr_is_incoming()) {
+        s->uevent_fd = cpr_find_fd(cpr_name, 0);
+        if (s->uevent_fd < 0) {
+            error_setg(errp,
+                       "vhost-blk: no preserved uevent socket to restore");
+            return false;
+        }
+    } else {
+        s->uevent_fd = qemu_open(s->conf.ueventfd, O_RDWR, errp);
+        if (s->uevent_fd < 0) {
+            error_prepend(errp, "vhost-blk: unable to open uevent socket: ");
+            return false;
+        }
+
+        if (!vhost_blk_uevent_check(s->uevent_fd, s->conf.ueventfd, errp)) {
+            qemu_close(s->uevent_fd);
+            s->uevent_fd = -1;
+            return false;
+        }
+
+        /* Preserve the socket across a future cpr-exec qemu-update. */
+        cpr_save_fd(cpr_name, 0, s->uevent_fd);
     }
 
     s->resize_bh = qemu_bh_new(vhost_blk_resize_bh, s);
@@ -387,10 +404,14 @@ static bool vhost_blk_uevent_attach(VHostBlk *s, Error **errp)
 
 static void vhost_blk_uevent_detach(VHostBlk *s)
 {
+    g_autofree char *cpr_name = NULL;
+
     if (s->uevent_fd < 0) {
         return;
     }
 
+    cpr_name = object_get_canonical_path(OBJECT(s));
+    cpr_delete_fd(cpr_name, 0);
     qemu_set_fd_handler(s->uevent_fd, NULL, NULL, NULL);
     qemu_bh_delete(s->resize_bh);
     s->resize_bh = NULL;
-- 
2.43.5


^ permalink raw reply	[relevance 27%]

* [QEMU HCI-8.0 PATCH v2 2/5] vhost-blk: change backend setup
    2026-09-04 13:21 18% ` [QEMU HCI-8.0 PATCH v2 1/5] vhost-blk: do not double close vhostfd Andrey Zhadchenko
@ 2026-09-04 13:21 23% ` Andrey Zhadchenko
  2026-09-04 15:33  0%   ` Andrey Drobyshev
  2026-09-04 13:21 26% ` [QEMU HCI-8.0 PATCH v2 3/5] vhost-blk: add read-only flag Andrey Zhadchenko
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 200+ results
From: Andrey Zhadchenko @ 2026-09-04 13:21 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

Previously we used very ugly and incapsulation-breaking assignment
fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
It is wrong in a many ways, so let's rework this.

Patch changes default `drive` to new `devpath` option so device fd
is managed by vhost-blk itself. Unfortunately this way we need a
bit more preparational work: finding out disk length, etc. Don't
be too broad and just do the minimal work. Drop the generic block
device properties along with the block node: the kernel module
does all IO in terms of 512 sectors, so simply report 512 byte
logical/physical block size to the guest.
Also we lose resize, as this is tied to the block node, which is
now have no place in the setup. We will add this in the next
patches as well as RO mode.

https://virtuozzo.atlassian.net/browse/VSTOR-143437
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
v2:
 - moved double close fix to  'do not double close vhostfd' patch
 - dropped DEFINE_BLOCK_PROPERTIES_BASE exposition to the outside. Let's
just use defaults for now. We can later check if tweaking this values
makes any difference and add them back.
 - change vhost_blk_update_size() to return zero or error and move 'changed'
to a separate argument
 - changed lseek to BLKGETSIZE64 (we already use BLKROGET anyway)

 hw/block/vhost-blk.c          | 126 ++++++++++++++++++++--------------
 include/hw/virtio/vhost-blk.h |   5 +-
 2 files changed, 78 insertions(+), 53 deletions(-)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index 9bd49fef2d..24f4fbe2b6 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -24,23 +24,17 @@
 #include "system/system.h"
 #include "linux-headers/linux/vhost.h"
 #include <sys/ioctl.h>
-#include <linux/fs.h>
-#include "include/block/block_int-common.h"
 #include "system/runstate.h"
 
 static int vhost_blk_start(VirtIODevice *vdev)
 {
     VHostBlk *s = VHOST_BLK(vdev);
     struct vhost_vring_file backend;
-    int ret, i, nworkers, *fd;
+    int ret, i, nworkers;
     BusState *qbus = BUS(qdev_get_parent_bus(DEVICE(vdev)));
     VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
     char serial[VIRTIO_BLK_ID_BYTES] = {0};
 
-    bdrv_graph_rdlock_main_loop();
-    fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
-    bdrv_graph_rdunlock_main_loop();
-
     if (!k->set_guest_notifiers) {
         error_report("vhost-blk: binding does not support guest notifiers");
         return -ENOSYS;
@@ -92,7 +86,7 @@ static int vhost_blk_start(VirtIODevice *vdev)
 
     memset(&backend, 0, sizeof(backend));
     backend.index = 0;
-    backend.fd = *fd;
+    backend.fd = s->backend_fd;
     if (ioctl(s->vhostfd, VHOST_BLK_SET_BACKEND, &backend)) {
         error_report("vhost-blk: unable to set backend");
         ret = -errno;
@@ -208,29 +202,69 @@ static void vhost_blk_vm_state(void *opaque, bool running, RunState state)
     }
 }
 
-static void vhost_blk_resize_cb(void *opaque)
+static int vhost_blk_update_size(VHostBlk *s, bool *changed, Error **errp)
 {
-    VirtIODevice *vdev = opaque;
+    BlockConf *conf = &s->conf.conf;
+    uint64_t length;
+
+    if (ioctl(s->backend_fd, BLKGETSIZE64, &length) < 0) {
+        int error = errno;
+
+        error_setg_errno(errp, error,
+                         "vhost-blk: unable to determine size of '%s'",
+                         s->conf.devpath);
+        return -error;
+    }
+
+    *changed = s->length != length;
+    s->length = length;
+    conf->heads = 16;
+    conf->secs = 63;
+    conf->cyls = s->length / BDRV_SECTOR_SIZE /
+                 (conf->heads * conf->secs);
+    conf->cyls = MIN(MAX(conf->cyls, 2U), 16383U);
 
-    assert(qemu_get_current_aio_context() == qemu_get_aio_context());
-    virtio_notify_config(vdev);
+    return 0;
 }
 
-static void vhost_blk_resize(void *opaque)
+static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
 {
-    VirtIODevice *vdev = VIRTIO_DEVICE(opaque);
+    BlockConf *conf = &s->conf.conf;
+    struct stat st;
+    bool changed;
 
-    /*
-     * virtio_notify_config() needs to acquire the global mutex,
-     * so it can't be called from an iothread. Instead, schedule
-     * it to be run in the main context BH.
-     */
-    aio_bh_schedule_oneshot(qemu_get_aio_context(), vhost_blk_resize_cb, vdev);
-}
+    s->backend_fd = qemu_open(s->conf.devpath, O_RDWR, errp);
+    if (s->backend_fd < 0) {
+        error_prepend(errp, "vhost-blk: unable to open backend: ");
+        return false;
+    }
 
-static const BlockDevOps vhost_blk_block_ops = {
-    .resize_cb     = vhost_blk_resize,
-};
+    if (fstat(s->backend_fd, &st) < 0) {
+        error_setg_errno(errp, errno, "vhost-blk: unable to stat '%s'",
+                         s->conf.devpath);
+        goto fail;
+    }
+
+    if (!S_ISBLK(st.st_mode)) {
+        error_setg(errp, "vhost-blk: '%s' is not a block device",
+                   s->conf.devpath);
+        goto fail;
+    }
+
+    if (vhost_blk_update_size(s, &changed, errp) < 0) {
+        goto fail;
+    }
+
+    conf->logical_block_size = BDRV_SECTOR_SIZE;
+    conf->physical_block_size = BDRV_SECTOR_SIZE;
+
+    return true;
+
+fail:
+    qemu_close(s->backend_fd);
+    s->backend_fd = -1;
+    return false;
+}
 
 static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
 {
@@ -239,13 +273,8 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
     VhostBlkConf *conf = &s->conf;
     int i, ret;
 
-    if (!conf->conf.blk) {
-        error_setg(errp, "vhost-blk: drive property not set");
-        return;
-    }
-
-    if (!blk_is_inserted(conf->conf.blk)) {
-        error_setg(errp, "vhost-blk: device needs media, but drive is empty");
+    if (!conf->devpath) {
+        error_setg(errp, "vhost-blk: devpath property must be set");
         return;
     }
 
@@ -273,17 +302,7 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
         return;
     }
 
-    if (!blkconf_apply_backend_options(&conf->conf,
-                                       !blk_supports_write_perm(conf->conf.blk),
-                                       true, errp)) {
-        return;
-    }
-
-    if (!blkconf_geometry(&conf->conf, NULL, 65535, 255, 255, errp)) {
-        return;
-    }
-
-    if (!blkconf_blocksizes(&conf->conf, errp)) {
+    if (!vhost_blk_open_backend(s, errp)) {
         return;
     }
 
@@ -311,8 +330,6 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
         goto cleanup;
     }
 
-    blk_set_dev_ops(s->conf.conf.blk, &vhost_blk_block_ops, s);
-
     ret = vhost_dev_init(&s->dev, (void *)((size_t)s->vhostfd),
                          VHOST_BACKEND_TYPE_KERNEL, 0, NULL);
     if (ret < 0) {
@@ -334,6 +351,10 @@ cleanup:
         close(s->vhostfd);
         s->vhostfd = -1;
     }
+    if (s->backend_fd >= 0) {
+        qemu_close(s->backend_fd);
+        s->backend_fd = -1;
+    }
     for (i = 0; i < conf->num_queues; i++) {
         virtio_del_queue(vdev, i);
     }
@@ -349,6 +370,10 @@ static void vhost_blk_device_unrealize(DeviceState *dev)
     qemu_del_vm_change_state_handler(s->mighand);
     vhost_blk_set_status(vdev, 0);
     vhost_dev_cleanup(&s->dev);
+    if (s->backend_fd >= 0) {
+        qemu_close(s->backend_fd);
+        s->backend_fd = -1;
+    }
     g_free(s->dev.vqs);
     virtio_cleanup(vdev);
 }
@@ -381,10 +406,6 @@ static uint64_t vhost_blk_get_features(VirtIODevice *vdev,
 
     virtio_add_feature(&features, VIRTIO_F_VERSION_1);
 
-    if (!blk_is_writable(s->conf.conf.blk)) {
-        virtio_add_feature(&features, VIRTIO_BLK_F_RO);
-    }
-
     if (s->conf.num_queues > 1) {
         virtio_add_feature(&features, VIRTIO_BLK_F_MQ);
     }
@@ -403,7 +424,9 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
     int64_t length;
     int blk_size = conf->logical_block_size;
 
-    blk_get_geometry(s->conf.conf.blk, &capacity);
+    length = s->length;
+    capacity = length / BDRV_SECTOR_SIZE;
+
     memset(&blkcfg, 0, sizeof(blkcfg));
     virtio_stq_p(vdev, &blkcfg.capacity, capacity);
     virtio_stl_p(vdev, &blkcfg.seg_max, s->conf.queue_size - 2);
@@ -411,7 +434,6 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
     virtio_stl_p(vdev, &blkcfg.blk_size, blk_size);
     blkcfg.geometry.heads = conf->heads;
 
-    length = blk_getlength(s->conf.conf.blk);
     if (length > 0 && length / conf->heads / conf->secs % blk_size) {
         unsigned short mask;
 
@@ -430,7 +452,7 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
 }
 
 static const Property vhost_blk_properties[] = {
-    DEFINE_BLOCK_PROPERTIES(VHostBlk, conf.conf),
+    DEFINE_PROP_STRING("devpath", VHostBlk, conf.devpath),
     DEFINE_PROP_UINT16("num-queues", VHostBlk, conf.num_queues,
                        VHOST_BLK_AUTO_NUM_QUEUES),
     DEFINE_PROP_UINT16("queue-size", VHostBlk, conf.queue_size, 256),
@@ -475,6 +497,8 @@ static void vhost_blk_instance_init(Object *obj)
 {
     VHostBlk *s = VHOST_BLK(obj);
 
+    s->vhostfd = -1;
+    s->backend_fd = -1;
     device_add_bootindex_property(obj, &s->conf.conf.bootindex,
                                   "bootindex", "/disk@0,0",
                                   DEVICE(obj));
diff --git a/include/hw/virtio/vhost-blk.h b/include/hw/virtio/vhost-blk.h
index 0c7e212595..c194b421d9 100644
--- a/include/hw/virtio/vhost-blk.h
+++ b/include/hw/virtio/vhost-blk.h
@@ -14,7 +14,6 @@
 #include "standard-headers/linux/virtio_blk.h"
 #include "hw/block/block.h"
 #include "hw/virtio/vhost.h"
-#include "system/block-backend.h"
 
 #define TYPE_VHOST_BLK "vhost-blk"
 #define VHOST_BLK(obj) \
@@ -25,6 +24,7 @@
 
 typedef struct VhostBlkConf {
     BlockConf conf;
+    char *devpath;
     uint16_t num_queues;
     uint16_t queue_size;
     uint16_t num_threads;
@@ -37,10 +37,11 @@ typedef struct VHostBlk {
     VMChangeStateEntry *mighand;
     uint64_t host_features;
     uint64_t decided_features;
-    struct virtio_blk_config blkcfg;
     int vhostfd;
+    int backend_fd;
     struct vhost_dev dev;
     bool vhost_started;
+    uint64_t length;
 } VHostBlk;
 
 #endif
-- 
2.43.5


^ permalink raw reply	[relevance 23%]

* [QEMU HCI-8.0 PATCH v2 1/5] vhost-blk: do not double close vhostfd
  @ 2026-09-04 13:21 18% ` Andrey Zhadchenko
  2026-09-04 15:33  6%   ` Andrey Drobyshev
  2026-09-04 13:21 23% ` [QEMU HCI-8.0 PATCH v2 2/5] vhost-blk: change backend setup Andrey Zhadchenko
                   ` (3 subsequent siblings)
  4 siblings, 1 reply; 200+ results
From: Andrey Zhadchenko @ 2026-09-04 13:21 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

vhost_dev_init() closes the backend fd on failure, so the cleanup
path of vhost_blk_device_realize() closed vhostfd for the second
time. The cleanup path is also reachable with vhostfd never opened,
in which case we called close(-1).

Fixes: 0ff3cb5ff2 ("block: add vhost-blk backend")
https://virtuozzo.atlassian.net/browse/VSTOR-143437
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
 hw/block/vhost-blk.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index c52851fcf8..9bd49fef2d 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -318,6 +318,8 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
     if (ret < 0) {
         error_setg(errp, "vhost-blk: vhost initialization failed: %s",
                 strerror(-ret));
+        /* vhost_dev_init() closes vhostfd on failure */
+        s->vhostfd = -1;
         goto cleanup;
     }
 
@@ -328,7 +330,10 @@ cleanup:
             qemu_del_vm_change_state_handler(s->mighand);
     }
     g_free(s->dev.vqs);
-    close(s->vhostfd);
+    if (s->vhostfd >= 0) {
+        close(s->vhostfd);
+        s->vhostfd = -1;
+    }
     for (i = 0; i < conf->num_queues; i++) {
         virtio_del_queue(vdev, i);
     }
-- 
2.43.5


^ permalink raw reply	[relevance 18%]

* [QEMU HCI-8.0 PATCH v2 3/5] vhost-blk: add read-only flag
    2026-09-04 13:21 18% ` [QEMU HCI-8.0 PATCH v2 1/5] vhost-blk: do not double close vhostfd Andrey Zhadchenko
  2026-09-04 13:21 23% ` [QEMU HCI-8.0 PATCH v2 2/5] vhost-blk: change backend setup Andrey Zhadchenko
@ 2026-09-04 13:21 26% ` Andrey Zhadchenko
  2026-09-04 15:33  0%   ` Andrey Drobyshev
  2026-09-04 13:21 20% ` [QEMU HCI-8.0 PATCH v2 4/5] vhost-blk: watch the device for resize events Andrey Zhadchenko
  2026-09-04 13:21 27% ` [QEMU HCI-8.0 PATCH v2 5/5] vhost-blk: preserve the uevent socket across cpr-exec Andrey Zhadchenko
  4 siblings, 1 reply; 200+ results
From: Andrey Zhadchenko @ 2026-09-04 13:21 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

and set RO respectively. Also compare BLKROGET with the selected
mode and reject r/w if needed.

https://virtuozzo.atlassian.net/browse/VSTOR-143437
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
v2:
 - check BLKROGET only if rw was requested like posix_file

 hw/block/vhost-blk.c          | 26 +++++++++++++++++++++++++-
 include/hw/virtio/vhost-blk.h |  1 +
 2 files changed, 26 insertions(+), 1 deletion(-)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index 24f4fbe2b6..04a7013e52 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -24,6 +24,7 @@
 #include "system/system.h"
 #include "linux-headers/linux/vhost.h"
 #include <sys/ioctl.h>
+#include <linux/fs.h>
 #include "system/runstate.h"
 
 static int vhost_blk_start(VirtIODevice *vdev)
@@ -232,8 +233,9 @@ static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
     BlockConf *conf = &s->conf.conf;
     struct stat st;
     bool changed;
+    int open_flags = s->conf.readonly ? O_RDONLY : O_RDWR;
 
-    s->backend_fd = qemu_open(s->conf.devpath, O_RDWR, errp);
+    s->backend_fd = qemu_open(s->conf.devpath, open_flags, errp);
     if (s->backend_fd < 0) {
         error_prepend(errp, "vhost-blk: unable to open backend: ");
         return false;
@@ -251,6 +253,23 @@ static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
         goto fail;
     }
 
+    if (!s->conf.readonly) {
+        int readonly;
+
+        if (ioctl(s->backend_fd, BLKROGET, &readonly) < 0) {
+            error_setg_errno(errp, errno,
+                             "vhost-blk: unable to get read-only status of "
+                             "'%s'", s->conf.devpath);
+            goto fail;
+        }
+
+        if (readonly) {
+            error_setg_errno(errp, EROFS, "vhost-blk: '%s' is not writable",
+                             s->conf.devpath);
+            goto fail;
+        }
+    }
+
     if (vhost_blk_update_size(s, &changed, errp) < 0) {
         goto fail;
     }
@@ -406,6 +425,10 @@ static uint64_t vhost_blk_get_features(VirtIODevice *vdev,
 
     virtio_add_feature(&features, VIRTIO_F_VERSION_1);
 
+    if (s->conf.readonly) {
+        virtio_add_feature(&features, VIRTIO_BLK_F_RO);
+    }
+
     if (s->conf.num_queues > 1) {
         virtio_add_feature(&features, VIRTIO_BLK_F_MQ);
     }
@@ -453,6 +476,7 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
 
 static const Property vhost_blk_properties[] = {
     DEFINE_PROP_STRING("devpath", VHostBlk, conf.devpath),
+    DEFINE_PROP_BOOL("read-only", VHostBlk, conf.readonly, false),
     DEFINE_PROP_UINT16("num-queues", VHostBlk, conf.num_queues,
                        VHOST_BLK_AUTO_NUM_QUEUES),
     DEFINE_PROP_UINT16("queue-size", VHostBlk, conf.queue_size, 256),
diff --git a/include/hw/virtio/vhost-blk.h b/include/hw/virtio/vhost-blk.h
index c194b421d9..c6646f5845 100644
--- a/include/hw/virtio/vhost-blk.h
+++ b/include/hw/virtio/vhost-blk.h
@@ -25,6 +25,7 @@
 typedef struct VhostBlkConf {
     BlockConf conf;
     char *devpath;
+    bool readonly;
     uint16_t num_queues;
     uint16_t queue_size;
     uint16_t num_threads;
-- 
2.43.5


^ permalink raw reply	[relevance 26%]

* [QEMU HCI-8.0 PATCH v2 4/5] vhost-blk: watch the device for resize events
                     ` (2 preceding siblings ...)
  2026-09-04 13:21 26% ` [QEMU HCI-8.0 PATCH v2 3/5] vhost-blk: add read-only flag Andrey Zhadchenko
@ 2026-09-04 13:21 20% ` Andrey Zhadchenko
  2026-09-04 15:33  6%   ` Andrey Drobyshev
  2026-09-04 13:21 27% ` [QEMU HCI-8.0 PATCH v2 5/5] vhost-blk: preserve the uevent socket across cpr-exec Andrey Zhadchenko
  4 siblings, 1 reply; 200+ results
From: Andrey Zhadchenko @ 2026-09-04 13:21 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

Resize was tied to block node, which we removed a few patches ago.
Luckily we can make resize automated: receive an uevent socket
from the management layer via the new "ueventfd" property (e.g. a
/dev/fdset/N path), watch it for relevant netlink messages and
call virtio_notify_config() if we detect a capacity change.

The socket is set up (possibly with filter) by management layer.
We only need to check that it is netlink and do some message
filtering.

When the property is not set, capacity changes are not detected.

https://virtuozzo.atlassian.net/browse/VSTOR-143437
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
v2:
 - rework patch a bit: now we expect opened and filtered fd from
libvirt.

 hw/block/vhost-blk.c          | 181 ++++++++++++++++++++++++++++++++++
 include/hw/virtio/vhost-blk.h |   4 +
 2 files changed, 185 insertions(+)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index 04a7013e52..7cb842a859 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -10,7 +10,9 @@
 
 #include "qemu/osdep.h"
 #include "qapi/error.h"
+#include "qemu/cutils.h"
 #include "qemu/error-report.h"
+#include "qemu/main-loop.h"
 #include "qom/object.h"
 #include "hw/qdev-core.h"
 #include "hw/boards.h"
@@ -25,6 +27,7 @@
 #include "linux-headers/linux/vhost.h"
 #include <sys/ioctl.h>
 #include <linux/fs.h>
+#include <linux/netlink.h>
 #include "system/runstate.h"
 
 static int vhost_blk_start(VirtIODevice *vdev)
@@ -228,6 +231,173 @@ static int vhost_blk_update_size(VHostBlk *s, bool *changed, Error **errp)
     return 0;
 }
 
+static void vhost_blk_resize_bh(void *opaque)
+{
+    VHostBlk *s = opaque;
+    Error *local_err = NULL;
+    bool changed;
+
+    if (vhost_blk_update_size(s, &changed, &local_err) < 0) {
+        error_report_err(local_err);
+        return;
+    }
+
+    if (changed) {
+        virtio_notify_config(VIRTIO_DEVICE(s));
+    }
+}
+
+/*
+ * The uevent socket is created, bound and filtered by the management
+ * layer and passed to us via the "ueventfd" property.
+ */
+static void vhost_blk_uevent_read(void *opaque)
+{
+    VHostBlk *s = opaque;
+    char buffer[64 * 1024 + 1];
+
+    for (;;) {
+        struct sockaddr_nl source;
+        socklen_t source_len = sizeof(source);
+        uint64_t event_major = UINT64_MAX;
+        uint64_t event_minor = UINT64_MAX;
+        bool action_change = false;
+        bool subsystem_block = false;
+        bool resize = false;
+        char *field;
+        char *end;
+        ssize_t len;
+
+        memset(&source, 0, sizeof(source));
+        len = recvfrom(s->uevent_fd, buffer, sizeof(buffer) - 1,
+                       MSG_DONTWAIT, (struct sockaddr *)&source, &source_len);
+        if (len < 0) {
+            if (errno == EINTR) {
+                continue;
+            }
+            if (errno == ENOBUFS) {
+                /* Some events may be dropped, just re-check */
+                qemu_bh_schedule(s->resize_bh);
+                continue;
+            }
+            if (errno != EAGAIN && errno != EWOULDBLOCK) {
+                error_report("vhost-blk: unable to receive uevent: %s",
+                             strerror(errno));
+            }
+            return;
+        }
+
+        if (source.nl_family != AF_NETLINK || source.nl_pid != 0) {
+            continue;
+        }
+
+        buffer[len] = '\0';
+        field = buffer;
+        end = buffer + len;
+        while (field < end) {
+            size_t field_len = strnlen(field, end - field);
+
+            if (!strcmp(field, "ACTION=change")) {
+                action_change = true;
+            } else if (!strcmp(field, "SUBSYSTEM=block")) {
+                subsystem_block = true;
+            } else if (!strcmp(field, "RESIZE=1")) {
+                resize = true;
+            } else if (g_str_has_prefix(field, "MAJOR=")) {
+                uint64_t value;
+
+                if (!qemu_strtou64(field + strlen("MAJOR="), NULL, 10,
+                                   &value)) {
+                    event_major = value;
+                }
+            } else if (g_str_has_prefix(field, "MINOR=")) {
+                uint64_t value;
+
+                if (!qemu_strtou64(field + strlen("MINOR="), NULL, 10,
+                                   &value)) {
+                    event_minor = value;
+                }
+            }
+
+            if (field_len == end - field) {
+                break;
+            }
+            field += field_len + 1;
+        }
+
+        if (action_change && subsystem_block && resize &&
+            event_major == major(s->backend_rdev) &&
+            event_minor == minor(s->backend_rdev)) {
+            qemu_bh_schedule(s->resize_bh);
+        }
+    }
+}
+
+static bool vhost_blk_uevent_check(int fd, const char *src, Error **errp)
+{
+    socklen_t optlen;
+    int domain;
+    int protocol;
+
+    optlen = sizeof(domain);
+    if (getsockopt(fd, SOL_SOCKET, SO_DOMAIN, &domain, &optlen) < 0) {
+        error_setg_errno(errp, errno, "vhost-blk: '%s' is not a socket", src);
+        return false;
+    }
+
+    optlen = sizeof(protocol);
+    if (getsockopt(fd, SOL_SOCKET, SO_PROTOCOL, &protocol, &optlen) < 0) {
+        error_setg_errno(errp, errno,
+                         "vhost-blk: unable to get protocol of '%s'", src);
+        return false;
+    }
+
+    if (domain != AF_NETLINK || protocol != NETLINK_KOBJECT_UEVENT) {
+        error_setg(errp,
+                   "vhost-blk: '%s' is not a NETLINK_KOBJECT_UEVENT socket",
+                   src);
+        return false;
+    }
+
+    return true;
+}
+
+static bool vhost_blk_uevent_attach(VHostBlk *s, Error **errp)
+{
+    if (!s->conf.ueventfd) {
+        return true;
+    }
+
+    s->uevent_fd = qemu_open(s->conf.ueventfd, O_RDWR, errp);
+    if (s->uevent_fd < 0) {
+        error_prepend(errp, "vhost-blk: unable to open uevent socket: ");
+        return false;
+    }
+
+    if (!vhost_blk_uevent_check(s->uevent_fd, s->conf.ueventfd, errp)) {
+        qemu_close(s->uevent_fd);
+        s->uevent_fd = -1;
+        return false;
+    }
+
+    s->resize_bh = qemu_bh_new(vhost_blk_resize_bh, s);
+    qemu_set_fd_handler(s->uevent_fd, vhost_blk_uevent_read, NULL, s);
+    return true;
+}
+
+static void vhost_blk_uevent_detach(VHostBlk *s)
+{
+    if (s->uevent_fd < 0) {
+        return;
+    }
+
+    qemu_set_fd_handler(s->uevent_fd, NULL, NULL, NULL);
+    qemu_bh_delete(s->resize_bh);
+    s->resize_bh = NULL;
+    qemu_close(s->uevent_fd);
+    s->uevent_fd = -1;
+}
+
 static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
 {
     BlockConf *conf = &s->conf.conf;
@@ -252,6 +422,7 @@ static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
                    s->conf.devpath);
         goto fail;
     }
+    s->backend_rdev = st.st_rdev;
 
     if (!s->conf.readonly) {
         int readonly;
@@ -325,6 +496,12 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
         return;
     }
 
+    if (!vhost_blk_uevent_attach(s, errp)) {
+        qemu_close(s->backend_fd);
+        s->backend_fd = -1;
+        return;
+    }
+
     s->dev.nvqs = conf->num_queues;
     s->dev.max_queues = conf->num_queues;
     s->dev.vqs = g_new0(struct vhost_virtqueue, s->dev.nvqs);
@@ -374,6 +551,7 @@ cleanup:
         qemu_close(s->backend_fd);
         s->backend_fd = -1;
     }
+    vhost_blk_uevent_detach(s);
     for (i = 0; i < conf->num_queues; i++) {
         virtio_del_queue(vdev, i);
     }
@@ -386,6 +564,7 @@ static void vhost_blk_device_unrealize(DeviceState *dev)
     VirtIODevice *vdev = VIRTIO_DEVICE(dev);
     VHostBlk *s = VHOST_BLK(dev);
 
+    vhost_blk_uevent_detach(s);
     qemu_del_vm_change_state_handler(s->mighand);
     vhost_blk_set_status(vdev, 0);
     vhost_dev_cleanup(&s->dev);
@@ -476,6 +655,7 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
 
 static const Property vhost_blk_properties[] = {
     DEFINE_PROP_STRING("devpath", VHostBlk, conf.devpath),
+    DEFINE_PROP_STRING("ueventfd", VHostBlk, conf.ueventfd),
     DEFINE_PROP_BOOL("read-only", VHostBlk, conf.readonly, false),
     DEFINE_PROP_UINT16("num-queues", VHostBlk, conf.num_queues,
                        VHOST_BLK_AUTO_NUM_QUEUES),
@@ -523,6 +703,7 @@ static void vhost_blk_instance_init(Object *obj)
 
     s->vhostfd = -1;
     s->backend_fd = -1;
+    s->uevent_fd = -1;
     device_add_bootindex_property(obj, &s->conf.conf.bootindex,
                                   "bootindex", "/disk@0,0",
                                   DEVICE(obj));
diff --git a/include/hw/virtio/vhost-blk.h b/include/hw/virtio/vhost-blk.h
index c6646f5845..e3ad92bc6e 100644
--- a/include/hw/virtio/vhost-blk.h
+++ b/include/hw/virtio/vhost-blk.h
@@ -25,6 +25,7 @@
 typedef struct VhostBlkConf {
     BlockConf conf;
     char *devpath;
+    char *ueventfd;
     bool readonly;
     uint16_t num_queues;
     uint16_t queue_size;
@@ -40,9 +41,12 @@ typedef struct VHostBlk {
     uint64_t decided_features;
     int vhostfd;
     int backend_fd;
+    int uevent_fd;
     struct vhost_dev dev;
     bool vhost_started;
     uint64_t length;
+    uint64_t backend_rdev;
+    QEMUBH *resize_bh;
 } VHostBlk;
 
 #endif
-- 
2.43.5


^ permalink raw reply	[relevance 20%]

* Re: [QEMU HCI-8.0 PATCH v2 1/5] vhost-blk: do not double close vhostfd
  2026-09-04 13:21 18% ` [QEMU HCI-8.0 PATCH v2 1/5] vhost-blk: do not double close vhostfd Andrey Zhadchenko
@ 2026-09-04 15:33  6%   ` Andrey Drobyshev
  0 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-04 15:33 UTC (permalink / raw)
  To: Andrey Zhadchenko; +Cc: svt-core, den, andrey.drobyshev

> vhost_dev_init() closes the backend fd on failure, so the cleanup
> path of vhost_blk_device_realize() closed vhostfd for the second
> time. The cleanup path is also reachable with vhostfd never opened,
> in which case we called close(-1).
> 
> Fixes: 0ff3cb5ff2 ("block: add vhost-blk backend")
> https://virtuozzo.atlassian.net/browse/VSTOR-143437
> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
>
> diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
> index c52851fcf8b..9bd49fef2da 100644
> --- a/hw/block/vhost-blk.c
> +++ b/hw/block/vhost-blk.c
> @@ -318,6 +318,8 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
>      if (ret < 0) {
>          error_setg(errp, "vhost-blk: vhost initialization failed: %s",
>                  strerror(-ret));
> +        /* vhost_dev_init() closes vhostfd on failure */
> +        s->vhostfd = -1;
>          goto cleanup;
>      }
>  
> @@ -328,7 +330,10 @@ cleanup:
>              qemu_del_vm_change_state_handler(s->mighand);
>      }
>      g_free(s->dev.vqs);

Not related, but while we're here:
  vhost_blk_device_realize()
    s->dev.vqs = g_new0()
    vhost_dev_init() fails ->
      vhost_dev_cleanup(s->dev)
        memset(s->dev, 0)
    g_free(s->dev.vqs)

Result: g_free(NULL) does nothing, array allocated with g_new0() leaks.
Worth another patch.  Or if it "Fixes:" the same commit - might fold
into the same patch.

Andrey

-- 
Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

^ permalink raw reply	[relevance 6%]

* Re: [QEMU HCI-8.0 PATCH v2 3/5] vhost-blk: add read-only flag
  2026-09-04 13:21 26% ` [QEMU HCI-8.0 PATCH v2 3/5] vhost-blk: add read-only flag Andrey Zhadchenko
@ 2026-09-04 15:33  0%   ` Andrey Drobyshev
  0 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-04 15:33 UTC (permalink / raw)
  To: Andrey Zhadchenko; +Cc: svt-core, den, andrey.drobyshev

> and set RO respectively. Also compare BLKROGET with the selected

Please have commit message start their own sentences.

> mode and reject r/w if needed.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-143437
> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
>
> diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
> index 24f4fbe2b68..04a7013e521 100644
> --- a/hw/block/vhost-blk.c
> +++ b/hw/block/vhost-blk.c
> @@ -24,6 +24,7 @@
>  #include "system/system.h"
>  #include "linux-headers/linux/vhost.h"
>  #include <sys/ioctl.h>
> +#include <linux/fs.h>

Header belongs to patch 2.

>  #include "system/runstate.h"
>  
>  static int vhost_blk_start(VirtIODevice *vdev)
> @@ -232,8 +233,9 @@ static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
>      BlockConf *conf = &s->conf.conf;
>      struct stat st;
>      bool changed;
> +    int open_flags = s->conf.readonly ? O_RDONLY : O_RDWR;
>  
> -    s->backend_fd = qemu_open(s->conf.devpath, O_RDWR, errp);
> +    s->backend_fd = qemu_open(s->conf.devpath, open_flags, errp);
>      if (s->backend_fd < 0) {
>          error_prepend(errp, "vhost-blk: unable to open backend: ");
>          return false;
> @@ -251,6 +253,23 @@ static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
>          goto fail;
>      }
>  
> +    if (!s->conf.readonly) {
> +        int readonly;
> +
> +        if (ioctl(s->backend_fd, BLKROGET, &readonly) < 0) {
> +            error_setg_errno(errp, errno,
> +                             "vhost-blk: unable to get read-only status of "
> +                             "'%s'", s->conf.devpath);
> +            goto fail;
> +        }
> +
> +        if (readonly) {
> +            error_setg_errno(errp, EROFS, "vhost-blk: '%s' is not writable",
> +                             s->conf.devpath);
> +            goto fail;
> +        }
> +    }
> +
>      if (vhost_blk_update_size(s, &changed, errp) < 0) {
>          goto fail;
>      }
> @@ -406,6 +425,10 @@ static uint64_t vhost_blk_get_features(VirtIODevice *vdev,
>  
>      virtio_add_feature(&features, VIRTIO_F_VERSION_1);
>  
> +    if (s->conf.readonly) {
> +        virtio_add_feature(&features, VIRTIO_BLK_F_RO);
> +    }
> +
>      if (s->conf.num_queues > 1) {
>          virtio_add_feature(&features, VIRTIO_BLK_F_MQ);
>      }
> @@ -453,6 +476,7 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
>  
>  static const Property vhost_blk_properties[] = {
>      DEFINE_PROP_STRING("devpath", VHostBlk, conf.devpath),
> +    DEFINE_PROP_BOOL("read-only", VHostBlk, conf.readonly, false),

Not for this patch, but related: AFAIU currently "<readonly/>" in domain
XML is applied by libvirt to the block node.  Correct?  If so - we also
need to patch libvirt making sure it applies to the device.

Andrey

-- 
Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

^ permalink raw reply	[relevance 0%]

* Re: [QEMU HCI-8.0 PATCH v2 4/5] vhost-blk: watch the device for resize events
  2026-09-04 13:21 20% ` [QEMU HCI-8.0 PATCH v2 4/5] vhost-blk: watch the device for resize events Andrey Zhadchenko
@ 2026-09-04 15:33  6%   ` Andrey Drobyshev
  2026-09-08  0:31  0%     ` Andrey Zhadchenko
  0 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-09-04 15:33 UTC (permalink / raw)
  To: Andrey Zhadchenko; +Cc: svt-core, den, andrey.drobyshev

> Resize was tied to block node, which we removed a few patches ago.
> Luckily we can make resize automated: receive an uevent socket
> from the management layer via the new "ueventfd" property (e.g. a
> /dev/fdset/N path), watch it for relevant netlink messages and
> call virtio_notify_config() if we detect a capacity change.
> 
> The socket is set up (possibly with filter) by management layer.
> We only need to check that it is netlink and do some message
> filtering.
> 
> When the property is not set, capacity changes are not detected.

So libvirt must ALWAYS set it, and we fail in .realize() if it's not
set.  Correct?  Let's mention it.

> 
> https://virtuozzo.atlassian.net/browse/VSTOR-143437
> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
>
> diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
> index 04a7013e521..7cb842a859e 100644
> --- a/hw/block/vhost-blk.c
> +++ b/hw/block/vhost-blk.c
> @@ -10,7 +10,9 @@
>  
>  #include "qemu/osdep.h"
>  #include "qapi/error.h"
> +#include "qemu/cutils.h"
>  #include "qemu/error-report.h"
> +#include "qemu/main-loop.h"
>  #include "qom/object.h"
>  #include "hw/qdev-core.h"
>  #include "hw/boards.h"
> @@ -25,6 +27,7 @@
>  #include "linux-headers/linux/vhost.h"
>  #include <sys/ioctl.h>
>  #include <linux/fs.h>
> +#include <linux/netlink.h>
>  #include "system/runstate.h"
>  
>  static int vhost_blk_start(VirtIODevice *vdev)
> @@ -228,6 +231,173 @@ static int vhost_blk_update_size(VHostBlk *s, bool *changed, Error **errp)
>      return 0;
>  }
>  
> +static void vhost_blk_resize_bh(void *opaque)
> +{
> +    VHostBlk *s = opaque;
> +    Error *local_err = NULL;
> +    bool changed;
> +
> +    if (vhost_blk_update_size(s, &changed, &local_err) < 0) {
> +        error_report_err(local_err);
> +        return;
> +    }
> +
> +    if (changed) {
> +        virtio_notify_config(VIRTIO_DEVICE(s));
> +    }
> +}
> +
> +/*
> + * The uevent socket is created, bound and filtered by the management
> + * layer and passed to us via the "ueventfd" property.
> + */
> +static void vhost_blk_uevent_read(void *opaque)
> +{
> +    VHostBlk *s = opaque;
> +    char buffer[64 * 1024 + 1];

include/linux/kobject.h
33:#define UEVENT_BUFFER_SIZE           2048    /* buffer for the variables */

Looks like 64K is overkill, maybe 4K?

> +
> +    for (;;) {
> +        struct sockaddr_nl source;
> +        socklen_t source_len = sizeof(source);
> +        uint64_t event_major = UINT64_MAX;
> +        uint64_t event_minor = UINT64_MAX;
> +        bool action_change = false;
> +        bool subsystem_block = false;
> +        bool resize = false;
> +        char *field;
> +        char *end;
> +        ssize_t len;
> +
> +        memset(&source, 0, sizeof(source));
> +        len = recvfrom(s->uevent_fd, buffer, sizeof(buffer) - 1,
> +                       MSG_DONTWAIT, (struct sockaddr *)&source, &source_len);
> +        if (len < 0) {
> +            if (errno == EINTR) {
> +                continue;
> +            }
> +            if (errno == ENOBUFS) {
> +                /* Some events may be dropped, just re-check */
> +                qemu_bh_schedule(s->resize_bh);
> +                continue;
> +            }
> +            if (errno != EAGAIN && errno != EWOULDBLOCK) {

Should ENOMEM go here to?  And if it's an error that breaks the socket,
like EBADF or smth else - maybe spit the error and detach the handler?
What's the point of keeping it afterwards?

> +                error_report("vhost-blk: unable to receive uevent: %s",
> +                             strerror(errno));
> +            }
> +            return;
> +        }
> +
> +        if (source.nl_family != AF_NETLINK || source.nl_pid != 0) {
> +            continue;
> +        }
> +
> +        buffer[len] = '\0';
> +        field = buffer;
> +        end = buffer + len;
> +        while (field < end) {
> +            size_t field_len = strnlen(field, end - field);
> +
> +            if (!strcmp(field, "ACTION=change")) {
> +                action_change = true;
> +            } else if (!strcmp(field, "SUBSYSTEM=block")) {
> +                subsystem_block = true;
> +            } else if (!strcmp(field, "RESIZE=1")) {
> +                resize = true;
> +            } else if (g_str_has_prefix(field, "MAJOR=")) {
> +                uint64_t value;
> +
> +                if (!qemu_strtou64(field + strlen("MAJOR="), NULL, 10,
> +                                   &value)) {
> +                    event_major = value;
> +                }
> +            } else if (g_str_has_prefix(field, "MINOR=")) {
> +                uint64_t value;
> +
> +                if (!qemu_strtou64(field + strlen("MINOR="), NULL, 10,
> +                                   &value)) {
> +                    event_minor = value;
> +                }
> +            }
> +
> +            if (field_len == end - field) {
> +                break;
> +            }
> +            field += field_len + 1;
> +        }
> +
> +        if (action_change && subsystem_block && resize &&
> +            event_major == major(s->backend_rdev) &&
> +            event_minor == minor(s->backend_rdev)) {
> +            qemu_bh_schedule(s->resize_bh);
> +        }
> +    }
> +}
> +
> +static bool vhost_blk_uevent_check(int fd, const char *src, Error **errp)
> +{
> +    socklen_t optlen;
> +    int domain;
> +    int protocol;
> +
> +    optlen = sizeof(domain);
> +    if (getsockopt(fd, SOL_SOCKET, SO_DOMAIN, &domain, &optlen) < 0) {
> +        error_setg_errno(errp, errno, "vhost-blk: '%s' is not a socket", src);
> +        return false;
> +    }
> +
> +    optlen = sizeof(protocol);
> +    if (getsockopt(fd, SOL_SOCKET, SO_PROTOCOL, &protocol, &optlen) < 0) {
> +        error_setg_errno(errp, errno,
> +                         "vhost-blk: unable to get protocol of '%s'", src);
> +        return false;
> +    }
> +
> +    if (domain != AF_NETLINK || protocol != NETLINK_KOBJECT_UEVENT) {
> +        error_setg(errp,
> +                   "vhost-blk: '%s' is not a NETLINK_KOBJECT_UEVENT socket",
> +                   src);
> +        return false;
> +    }

How about also checking nl_groups? Should be 1 for multicast.  I.e.

  if (getsockname(fd, (struct sockaddr *)&addr, &addrlen) < 0) {
      error_setg_errno(...);
      return false;
  }
  if (addr.nl_groups != 1) {
      error_setg(errp, "vhost-blk: '%s' is not subscribed to kernel uevents",
                 src);
      return false;
  }

> +
> +    return true;
> +}
> +
> +static bool vhost_blk_uevent_attach(VHostBlk *s, Error **errp)
> +{
> +    if (!s->conf.ueventfd) {
> +        return true;
> +    }
> +
> +    s->uevent_fd = qemu_open(s->conf.ueventfd, O_RDWR, errp);

For conf values monitor_fd_param() is usually used, so how about

  s->uevent_fd = monitor_fd_param(monitor_cur(), s->conf.ueventfd, errp);

Andrey

-- 
Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

^ permalink raw reply	[relevance 6%]

* Re: [QEMU HCI-8.0 PATCH v2 2/5] vhost-blk: change backend setup
  2026-09-04 13:21 23% ` [QEMU HCI-8.0 PATCH v2 2/5] vhost-blk: change backend setup Andrey Zhadchenko
@ 2026-09-04 15:33  0%   ` Andrey Drobyshev
  0 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-04 15:33 UTC (permalink / raw)
  To: Andrey Zhadchenko; +Cc: svt-core, den, andrey.drobyshev

> Previously we used very ugly and incapsulation-breaking assignment
> fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
> It is wrong in a many ways, so let's rework this.
> 
> Patch changes default `drive` to new `devpath` option so device fd
> is managed by vhost-blk itself. Unfortunately this way we need a
> bit more preparational work: finding out disk length, etc. Don't
> be too broad and just do the minimal work. Drop the generic block
> device properties along with the block node: the kernel module
> does all IO in terms of 512 sectors, so simply report 512 byte
> logical/physical block size to the guest.
> Also we lose resize, as this is tied to the block node, which is
> now have no place in the setup. We will add this in the next
> patches as well as RO mode.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-143437
> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
>
> diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
> index 9bd49fef2da..24f4fbe2b68 100644
> --- a/hw/block/vhost-blk.c
> +++ b/hw/block/vhost-blk.c
> @@ -24,23 +24,17 @@
>  #include "system/system.h"
>  #include "linux-headers/linux/vhost.h"
>  #include <sys/ioctl.h>
> -#include <linux/fs.h>

Nit: next patch brings this header back, just keep it here.

> -#include "include/block/block_int-common.h"
>  #include "system/runstate.h"
>  
>  static int vhost_blk_start(VirtIODevice *vdev)
>  {
>      VHostBlk *s = VHOST_BLK(vdev);
>      struct vhost_vring_file backend;
> -    int ret, i, nworkers, *fd;
> +    int ret, i, nworkers;
>      BusState *qbus = BUS(qdev_get_parent_bus(DEVICE(vdev)));
>      VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
>      char serial[VIRTIO_BLK_ID_BYTES] = {0};
>  
> -    bdrv_graph_rdlock_main_loop();
> -    fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
> -    bdrv_graph_rdunlock_main_loop();
> -
>      if (!k->set_guest_notifiers) {
>          error_report("vhost-blk: binding does not support guest notifiers");
>          return -ENOSYS;
> @@ -92,7 +86,7 @@ static int vhost_blk_start(VirtIODevice *vdev)
>  
>      memset(&backend, 0, sizeof(backend));
>      backend.index = 0;
> -    backend.fd = *fd;
> +    backend.fd = s->backend_fd;
>      if (ioctl(s->vhostfd, VHOST_BLK_SET_BACKEND, &backend)) {
>          error_report("vhost-blk: unable to set backend");
>          ret = -errno;
> @@ -208,29 +202,69 @@ static void vhost_blk_vm_state(void *opaque, bool running, RunState state)
>      }
>  }
>  
> -static void vhost_blk_resize_cb(void *opaque)
> +static int vhost_blk_update_size(VHostBlk *s, bool *changed, Error **errp)
>  {
> -    VirtIODevice *vdev = opaque;
> +    BlockConf *conf = &s->conf.conf;
> +    uint64_t length;
> +
> +    if (ioctl(s->backend_fd, BLKGETSIZE64, &length) < 0) {
> +        int error = errno;

This is redundant, error_setg_errno() preserves errno value.

> +
> +        error_setg_errno(errp, error,
> +                         "vhost-blk: unable to determine size of '%s'",
> +                         s->conf.devpath);
> +        return -error;

This is wrong error handling.  Let this function return 'bool changed'.
Then callers of vhost_blk_update_size() should check whether Error **errp
was set to smth, and either process the error themselves or propagate it
further.  That's how it's usually done in QEMU codebase.

> +    }
> +
> +    *changed = s->length != length;
> +    s->length = length;
> +    conf->heads = 16;
> +    conf->secs = 63;
> +    conf->cyls = s->length / BDRV_SECTOR_SIZE /
> +                 (conf->heads * conf->secs);
> +    conf->cyls = MIN(MAX(conf->cyls, 2U), 16383U);
>  
> -    assert(qemu_get_current_aio_context() == qemu_get_aio_context());
> -    virtio_notify_config(vdev);
> +    return 0;
>  }
>  
> -static void vhost_blk_resize(void *opaque)
> +static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
>  {
> -    VirtIODevice *vdev = VIRTIO_DEVICE(opaque);
> +    BlockConf *conf = &s->conf.conf;
> +    struct stat st;
> +    bool changed;
>  
> -    /*
> -     * virtio_notify_config() needs to acquire the global mutex,
> -     * so it can't be called from an iothread. Instead, schedule
> -     * it to be run in the main context BH.
> -     */
> -    aio_bh_schedule_oneshot(qemu_get_aio_context(), vhost_blk_resize_cb, vdev);
> -}
> +    s->backend_fd = qemu_open(s->conf.devpath, O_RDWR, errp);
> +    if (s->backend_fd < 0) {
> +        error_prepend(errp, "vhost-blk: unable to open backend: ");
> +        return false;
> +    }
>  
> -static const BlockDevOps vhost_blk_block_ops = {
> -    .resize_cb     = vhost_blk_resize,
> -};
> +    if (fstat(s->backend_fd, &st) < 0) {
> +        error_setg_errno(errp, errno, "vhost-blk: unable to stat '%s'",
> +                         s->conf.devpath);
> +        goto fail;
> +    }
> +
> +    if (!S_ISBLK(st.st_mode)) {
> +        error_setg(errp, "vhost-blk: '%s' is not a block device",
> +                   s->conf.devpath);
> +        goto fail;
> +    }
> +
> +    if (vhost_blk_update_size(s, &changed, errp) < 0) {
> +        goto fail;
> +    }
> +
> +    conf->logical_block_size = BDRV_SECTOR_SIZE;
> +    conf->physical_block_size = BDRV_SECTOR_SIZE;
> +
> +    return true;
> +
> +fail:
> +    qemu_close(s->backend_fd);
> +    s->backend_fd = -1;
> +    return false;
> +}
>  
>  static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
>  {
> @@ -239,13 +273,8 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
>      VhostBlkConf *conf = &s->conf;
>      int i, ret;
>  
> -    if (!conf->conf.blk) {
> -        error_setg(errp, "vhost-blk: drive property not set");
> -        return;
> -    }
> -
> -    if (!blk_is_inserted(conf->conf.blk)) {
> -        error_setg(errp, "vhost-blk: device needs media, but drive is empty");
> +    if (!conf->devpath) {
> +        error_setg(errp, "vhost-blk: devpath property must be set");
>          return;
>      }
>  
> @@ -273,17 +302,7 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
>          return;
>      }
>  
> -    if (!blkconf_apply_backend_options(&conf->conf,
> -                                       !blk_supports_write_perm(conf->conf.blk),
> -                                       true, errp)) {
> -        return;
> -    }
> -
> -    if (!blkconf_geometry(&conf->conf, NULL, 65535, 255, 255, errp)) {
> -        return;
> -    }
> -
> -    if (!blkconf_blocksizes(&conf->conf, errp)) {
> +    if (!vhost_blk_open_backend(s, errp)) {
>          return;
>      }
>  
> @@ -311,8 +330,6 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
>          goto cleanup;
>      }
>  
> -    blk_set_dev_ops(s->conf.conf.blk, &vhost_blk_block_ops, s);
> -
>      ret = vhost_dev_init(&s->dev, (void *)((size_t)s->vhostfd),
>                           VHOST_BACKEND_TYPE_KERNEL, 0, NULL);
>      if (ret < 0) {
> @@ -334,6 +351,10 @@ cleanup:
>          close(s->vhostfd);
>          s->vhostfd = -1;
>      }
> +    if (s->backend_fd >= 0) {
> +        qemu_close(s->backend_fd);
> +        s->backend_fd = -1;
> +    }
>      for (i = 0; i < conf->num_queues; i++) {
>          virtio_del_queue(vdev, i);
>      }
> @@ -349,6 +370,10 @@ static void vhost_blk_device_unrealize(DeviceState *dev)
>      qemu_del_vm_change_state_handler(s->mighand);
>      vhost_blk_set_status(vdev, 0);
>      vhost_dev_cleanup(&s->dev);
> +    if (s->backend_fd >= 0) {
> +        qemu_close(s->backend_fd);
> +        s->backend_fd = -1;
> +    }
>      g_free(s->dev.vqs);
>      virtio_cleanup(vdev);
>  }
> @@ -381,10 +406,6 @@ static uint64_t vhost_blk_get_features(VirtIODevice *vdev,
>  
>      virtio_add_feature(&features, VIRTIO_F_VERSION_1);
>  
> -    if (!blk_is_writable(s->conf.conf.blk)) {
> -        virtio_add_feature(&features, VIRTIO_BLK_F_RO);
> -    }
> -
>      if (s->conf.num_queues > 1) {
>          virtio_add_feature(&features, VIRTIO_BLK_F_MQ);
>      }
> @@ -403,7 +424,9 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
>      int64_t length;
>      int blk_size = conf->logical_block_size;
>  
> -    blk_get_geometry(s->conf.conf.blk, &capacity);
> +    length = s->length;
> +    capacity = length / BDRV_SECTOR_SIZE;
> +
>      memset(&blkcfg, 0, sizeof(blkcfg));
>      virtio_stq_p(vdev, &blkcfg.capacity, capacity);
>      virtio_stl_p(vdev, &blkcfg.seg_max, s->conf.queue_size - 2);
> @@ -411,7 +434,6 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
>      virtio_stl_p(vdev, &blkcfg.blk_size, blk_size);
>      blkcfg.geometry.heads = conf->heads;
>  
> -    length = blk_getlength(s->conf.conf.blk);
>      if (length > 0 && length / conf->heads / conf->secs % blk_size) {
>          unsigned short mask;
>  
> @@ -430,7 +452,7 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
>  }
>  
>  static const Property vhost_blk_properties[] = {
> -    DEFINE_BLOCK_PROPERTIES(VHostBlk, conf.conf),

Removing these props currently results into:

  error: Failed to start domain
  Property 'vhost-blk-pci.physical_block_size' not found

I understand we also patch libvirt so that it doesn't send those
block_size props, as well as write-cache etc.  Let's mention that in
commit message.

> +    DEFINE_PROP_STRING("devpath", VHostBlk, conf.devpath),
>      DEFINE_PROP_UINT16("num-queues", VHostBlk, conf.num_queues,
>                         VHOST_BLK_AUTO_NUM_QUEUES),
>      DEFINE_PROP_UINT16("queue-size", VHostBlk, conf.queue_size, 256),
> @@ -475,6 +497,8 @@ static void vhost_blk_instance_init(Object *obj)
>  {
>      VHostBlk *s = VHOST_BLK(obj);
>  
> +    s->vhostfd = -1;

Nit: ideally belongs to patch #1.  Not a big deal, but if you do a respin -
put it there.

Andrey

-- 
Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

^ permalink raw reply	[relevance 0%]

* Re: [QEMU HCI-8.0 PATCH v2 5/5] vhost-blk: preserve the uevent socket across cpr-exec
  2026-09-04 13:21 27% ` [QEMU HCI-8.0 PATCH v2 5/5] vhost-blk: preserve the uevent socket across cpr-exec Andrey Zhadchenko
@ 2026-09-04 15:33  0%   ` Andrey Drobyshev
  0 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-04 15:33 UTC (permalink / raw)
  To: Andrey Zhadchenko; +Cc: svt-core, den, andrey.drobyshev

> qemu-update uses cpr-exec migration: QEMU re-execs itself in place.
> A plain monitor fdset descriptor like our uevent socket does not
> survive that (unlike the cpr_save_fd()'d tap/vhost fds), so without
> help the resized-device notifications would silently stop until the
> next full VM start.
> 
> Preserve it the same way the net backends do: on cold boot
> cpr_save_fd() the socket under the device's canonical path, and on
> the re-exec'd (incoming) side adopt it with cpr_find_fd() instead of
> reopening a command line fd that no longer resolves. The management
> layer passes a placeholder for the "ueventfd" property in that case.
> Drop the saved descriptor on teardown so it does not outlive the
> device.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-143437
> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
>
> diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
> index 7cb842a859e..eca12356ae5 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 "migration/cpr.h"
>  #include "system/runstate.h"
>  
>  static int vhost_blk_start(VirtIODevice *vdev)
> @@ -364,20 +365,36 @@ static bool vhost_blk_uevent_check(int fd, const char *src, Error **errp)
>  
>  static bool vhost_blk_uevent_attach(VHostBlk *s, Error **errp)
>  {
> +    g_autofree char *cpr_name = NULL;
> +
>      if (!s->conf.ueventfd) {
>          return true;
>      }
>  
> -    s->uevent_fd = qemu_open(s->conf.ueventfd, O_RDWR, errp);
> -    if (s->uevent_fd < 0) {
> -        error_prepend(errp, "vhost-blk: unable to open uevent socket: ");
> -        return false;
> -    }
> +    cpr_name = object_get_canonical_path(OBJECT(s));

That name is gonna be "/machine/peripheral/...".  Let's do CPR
consistently with other devices.  E.g. see how it's done in
vhost_vsock_device_realize():

  DeviceState *proxy = qdev_get_parent_bus(DEVICE(vsock))->parent;
  ...
  /* Add migration blockers if proxy->id isn't present */

For vhost-blk proxy->id is likely gonna be "virtio-disk0".

Also, in this case, since it's not a vhostfd or backend FD, but an
ueventfd, I'd prefer adding it as a suffix, as it's done for other
devices.  So CPR key should end up looking like "virtio-disk0_ueventfd".

>  
> -    if (!vhost_blk_uevent_check(s->uevent_fd, s->conf.ueventfd, errp)) {
> -        qemu_close(s->uevent_fd);
> -        s->uevent_fd = -1;
> -        return false;
> +    if (cpr_is_incoming()) {
> +        s->uevent_fd = cpr_find_fd(cpr_name, 0);
> +        if (s->uevent_fd < 0) {
> +            error_setg(errp,
> +                       "vhost-blk: no preserved uevent socket to restore");
> +            return false;
> +        }
> +    } else {
> +        s->uevent_fd = qemu_open(s->conf.ueventfd, O_RDWR, errp);
> +        if (s->uevent_fd < 0) {
> +            error_prepend(errp, "vhost-blk: unable to open uevent socket: ");
> +            return false;
> +        }
> +
> +        if (!vhost_blk_uevent_check(s->uevent_fd, s->conf.ueventfd, errp)) {

Don't we want the same validation for cpr_is_incoming() case?

Andrey

-- 
Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

^ permalink raw reply	[relevance 0%]

* [QEMU HCI-8.0 PATCH v2 02/13] usb-host: don't leak hostdev FD on open failure #VSTOR-137800
  @ 2026-09-04 19:04 29% ` Andrey Drobyshev
  2026-09-04 19:04 13% ` [QEMU HCI-8.0 PATCH v2 03/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-04 19:04 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 | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b74670ae256..a8e6f142ec0 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -974,11 +974,11 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
 #if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
         trace_usb_host_open_hostfd(hostfd);
 
+        s->hostfd = hostfd;
         rc = libusb_wrap_sys_device(ctx, hostfd, &s->dh);
         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 +1066,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 29%]

* [QEMU HCI-8.0 PATCH v2 03/13] usb-host: add migration blocker for CPR modes #VSTOR-137800
    2026-09-04 19:04 29% ` [QEMU HCI-8.0 PATCH v2 02/13] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
@ 2026-09-04 19:04 13% ` Andrey Drobyshev
  2026-09-04 19:04 25% ` [QEMU HCI-8.0 PATCH v2 04/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-04 19:04 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 a8e6f142ec0..86f96087085 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;
@@ -1254,6 +1256,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)
@@ -1275,6 +1289,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%]

* [QEMU HCI-8.0 PATCH v2 04/13] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800
    2026-09-04 19:04 29% ` [QEMU HCI-8.0 PATCH v2 02/13] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
  2026-09-04 19:04 13% ` [QEMU HCI-8.0 PATCH v2 03/13] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
@ 2026-09-04 19:04 25% ` Andrey Drobyshev
  2026-09-04 19:04 25% ` [QEMU HCI-8.0 PATCH v2 05/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-04 19:04 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 | 53 ++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 51 insertions(+), 2 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 86f96087085..eaacfd34c54 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,38 @@ static void usb_host_ep_update(USBHostDevice *s)
     libusb_free_config_descriptor(conf);
 }
 
+static char *usb_host_cpr_fd_name(USBHostDevice *s)
+{
+    /*
+     * Prefix the CPR fd registry key so it can't collide with a chardev
+     * label or netdev id in the same flat namespace.
+     */
+    return DEVICE(s)->id ?
+        g_strdup_printf("usb-host/%s", DEVICE(s)->id) : NULL;
+}
+
+static int usb_host_cpr_find_fd(USBHostDevice *s)
+{
+    g_autofree char *name = usb_host_cpr_fd_name(s);
+    return name ? cpr_find_fd(name, 0) : -1;
+}
+
+static void usb_host_cpr_save_fd(USBHostDevice *s, int fd)
+{
+    g_autofree char *name = usb_host_cpr_fd_name(s);
+    if (name) {
+        cpr_save_fd(name, 0, fd);
+    }
+}
+
+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);
@@ -1069,6 +1102,7 @@ fail:
         s->dev = NULL;
     }
     if (s->hostfd != -1) {
+        usb_host_cpr_delete_fd(s);
         close(s->hostfd);
         s->hostfd = -1;
     }
@@ -1130,6 +1164,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;
     }
@@ -1218,9 +1253,13 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
     if (s->hostdevice) {
         int fd;
         s->needs_autoscan = false;
-        fd = qemu_open(s->hostdevice, O_RDWR, errp);
+        fd = usb_host_cpr_find_fd(s);
         if (fd < 0) {
-            return;
+            fd = qemu_open(s->hostdevice, O_RDWR, errp);
+            if (fd < 0) {
+                return;
+            }
+            usb_host_cpr_save_fd(s, fd);
         }
         rc = usb_host_open(s, NULL, fd);
         if (rc < 0) {
@@ -1757,6 +1796,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 25%]

* [QEMU HCI-8.0 PATCH v2 05/13] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800
                     ` (2 preceding siblings ...)
  2026-09-04 19:04 25% ` [QEMU HCI-8.0 PATCH v2 04/13] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
@ 2026-09-04 19:04 25% ` Andrey Drobyshev
  2026-09-04 19:04 17% ` [QEMU HCI-8.0 PATCH v2 06/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-04 19:04 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 eaacfd34c54..d695ba3170f 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1109,15 +1109,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));
@@ -1139,6 +1135,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%]

* [QEMU HCI-8.0 PATCH v2 07/13] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
                     ` (4 preceding siblings ...)
  2026-09-04 19:04 17% ` [QEMU HCI-8.0 PATCH v2 06/13] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
@ 2026-09-04 19:04 21% ` Andrey Drobyshev
  2026-09-04 19:04 22% ` [QEMU HCI-8.0 PATCH v2 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-04 19:04 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 | 75 ++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 73 insertions(+), 2 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index c8a893073a6..8483df0d12c 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;
@@ -1159,8 +1162,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)
 {
@@ -1171,6 +1179,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
              * must not complete the packet. */
@@ -1178,6 +1190,7 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
         }
         libusb_cancel_transfer(r->xfer);
     }
+    s->cpr_drained = true;
 
     usb_host_reap_xfers(s);
 
@@ -1288,6 +1301,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);
@@ -1365,6 +1425,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,
@@ -1398,6 +1466,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)
-- 
2.47.1


^ permalink raw reply	[relevance 21%]

* [QEMU HCI-8.0 PATCH v2 06/13] usb-host: drain in-flight URBs across CPR #VSTOR-137800
                     ` (3 preceding siblings ...)
  2026-09-04 19:04 25% ` [QEMU HCI-8.0 PATCH v2 05/13] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
@ 2026-09-04 19:04 17% ` Andrey Drobyshev
  2026-09-04 19:04 21% ` [QEMU HCI-8.0 PATCH v2 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-04 19:04 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, which is safe for mass storage.

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 | 79 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 79 insertions(+)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index d695ba3170f..c8a893073a6 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"
@@ -1145,6 +1146,68 @@ 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);
+    }
+
+    usb_host_reap_xfers(s);
+
+    /*
+     * 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);
@@ -1821,10 +1884,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 17%]

* [QEMU HCI-8.0 PATCH v2 08/13] usb-host: skip product-string read on CPR incoming #VSTOR-137800
                     ` (5 preceding siblings ...)
  2026-09-04 19:04 21% ` [QEMU HCI-8.0 PATCH v2 07/13] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
@ 2026-09-04 19:04 22% ` Andrey Drobyshev
  2026-09-04 19:04 20% ` [QEMU HCI-8.0 PATCH v2 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-04 19:04 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 8483df0d12c..3c760e9f8f1 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1077,7 +1077,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%]

* [QEMU HCI-8.0 PATCH v2 09/13] usb-host: hand off the device across cpr-transfer #VSTOR-137800
                     ` (6 preceding siblings ...)
  2026-09-04 19:04 22% ` [QEMU HCI-8.0 PATCH v2 08/13] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-09-04 19:04 20% ` Andrey Drobyshev
  2026-09-04 19:04  5% ` [QEMU HCI-8.0 PATCH v2 11/13] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
  2026-09-04 19:04 20% ` [QEMU HCI-8.0 PATCH v2 13/13] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-04 19:04 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 | 78 +++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 74 insertions(+), 4 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 3c760e9f8f1..59f2b151cd2 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;
@@ -1275,10 +1276,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);
     }
 }
@@ -1310,6 +1318,9 @@ static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
  * from the packet alone; complete them as errors and let the guest
  * driver retry.
  */
+static void usb_host_cpr_stop_events(void);
+static void usb_host_cpr_restart_events(void);
+
 static int usb_host_cpr_notifier(NotifierWithReturn *notifier,
                                  MigrationEvent *e, Error **errp)
 {
@@ -1322,6 +1333,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 */
@@ -1395,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 &&
@@ -1943,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;
     }
 
@@ -1955,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;
@@ -1966,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 20%]

* [QEMU HCI-8.0 PATCH v2 13/13] usb-host: make CPR migration blocker conditional #VSTOR-137800
                     ` (8 preceding siblings ...)
  2026-09-04 19:04  5% ` [QEMU HCI-8.0 PATCH v2 11/13] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-09-04 19:04 20% ` Andrey Drobyshev
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-04 19:04 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 d12e04c2768..da0ff801661 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%]

* [QEMU HCI-8.0 PATCH v2 11/13] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800
                     ` (7 preceding siblings ...)
  2026-09-04 19:04 20% ` [QEMU HCI-8.0 PATCH v2 09/13] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
@ 2026-09-04 19:04  5% ` Andrey Drobyshev
  2026-09-04 19:04 20% ` [QEMU HCI-8.0 PATCH v2 13/13] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-04 19:04 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 rescan is also the only place where we claim the interfaces in
libusb.  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 | 35 +++++++++++++++++++++++++++++++++++
 1 file changed, 35 insertions(+)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 59f2b151cd2..d12e04c2768 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1956,6 +1956,34 @@ 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 void usb_host_cpr_reclaim_interfaces(USBHostDevice *s)
+{
+    USBDevice *udev = USB_DEVICE(s);
+    struct libusb_config_descriptor *conf;
+    int i, n;
+
+    if (libusb_get_active_config_descriptor(s->dev, &conf) != 0) {
+        return;
+    }
+
+    n = MIN(conf->bNumInterfaces, USB_MAX_INTERFACES);
+    for (i = 0; i < n; i++) {
+        if (libusb_claim_interface(s->dh, i) == 0) {
+            s->ifs[i].claimed = true;
+        }
+    }
+
+    udev->ninterfaces = conf->bNumInterfaces;
+    udev->configuration = conf->bConfigurationValue;
+    libusb_free_config_descriptor(conf);
+}
+
 static int usb_host_post_load(void *opaque, int version_id)
 {
     USBHostDevice *dev = opaque;
@@ -1967,6 +1995,13 @@ 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.
+         */
+        usb_host_cpr_reclaim_interfaces(dev);
+        usb_host_ep_update(dev);
         usb_host_cpr_restart_events();
         return 0;
     }
-- 
2.47.1


^ permalink raw reply	[relevance 5%]

* [Devel] [PATCH VZ10 2/3] selftests/ve: Add more helpers
    2026-09-07 19:14  6% ` [Devel] [PATCH VZ10 1/3] selftests/ve: Add namespace management helpers Vladimir Riabchun
@ 2026-09-07 19:14  6% ` Vladimir Riabchun
  2026-09-09 11:03  4%   ` Vladimir Riabchun
  2026-09-07 19:14  4% ` [Devel] [PATCH VZ10 3/3] ve_namespace: Allow entering non-running VE namespace Vladimir Riabchun
  2 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-09-07 19:14 UTC (permalink / raw)
  To: Konstantin Khorenko; +Cc: devel

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>
---
 tools/testing/selftests/ve/ve_selftest.h | 78 +++++++++++++++++++++++++++++---
 1 file changed, 72 insertions(+), 6 deletions(-)

diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index b3c5bf63f34d..d35fec30c7b4 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -163,6 +163,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[20];
+
+	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)
 {
@@ -193,19 +201,31 @@ static inline int read_u64_at(int dirfd, const char *path,
 			      unsigned long long *out)
 {
 	char buf[32] = {0}, *end;
-	int fd, ret;
+	int ret;
 
-	fd = openat(dirfd, path, O_RDONLY);
-	if (fd < 0)
+	ret = read_file_at(dirfd, path, buf, sizeof(buf));
+	if (ret <= 0)
 		return -1;
 
-	ret = read(fd, buf, sizeof(buf) - 1);
-	close(fd);
+	errno = 0;
+	*out = strtoull(buf, &end, 10);
+	if (errno || end == buf)
+		return -1;
+	return 0;
+}
+
+static inline int read_s32_at(int dirfd, const char *path,
+			      int *out)
+{
+	char buf[32] = {0}, *end;
+	int ret;
+
+	ret = read_file_at(dirfd, path, buf, sizeof(buf));
 	if (ret <= 0)
 		return -1;
 
 	errno = 0;
-	*out = strtoull(buf, &end, 10);
+	*out = strtol(buf, &end, 10);
 	if (errno || end == buf)
 		return -1;
 	return 0;
@@ -254,6 +274,52 @@ static inline int enter_cgroup(int cgv2_fd, int ctid)
 	return ret;
 }
 
+/*
+ * Run function in VE cgroup and new namespaces.
+ *
+ * Namespaces are provided via unshare_flags.
+ * CLONE_NEWVE flag is set by this function.
+ * Return values:
+ *  -  0 if function returns zero
+ *  - -1 if function returns negative value
+ *  -  1 if setup fails or function returns positive value
+ */
+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 1;
+	}
+	if (pid == 0) {
+		int ret;
+
+		if (enter_cgroup(cgv2_fd, ctid) < 0) {
+			fprintf(stderr, "%s: enter_cgroup failed\n", __func__);
+			_exit(255);
+		}
+		if (enter_namespaces(ctx, flags) < 0)
+			_exit(255);
+
+		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) == 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.47.1

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

^ permalink raw reply	[relevance 6%]

* [Devel] [PATCH VZ10 1/3] selftests/ve: Add namespace management helpers
  @ 2026-09-07 19:14  6% ` Vladimir Riabchun
  2026-09-09 10:39  0%   ` Vladimir Riabchun
  2026-09-07 19:14  6% ` [Devel] [PATCH VZ10 2/3] selftests/ve: Add more helpers Vladimir Riabchun
  2026-09-07 19:14  4% ` [Devel] [PATCH VZ10 3/3] ve_namespace: Allow entering non-running VE namespace Vladimir Riabchun
  2 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-09-07 19:14 UTC (permalink / raw)
  To: Konstantin Khorenko; +Cc: devel

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>
---
 tools/testing/selftests/ve/ve_selftest.h | 118 +++++++++++++++++++++++++++++++
 1 file changed, 118 insertions(+)

diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index c53bf7900d20..b3c5bf63f34d 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -31,6 +31,124 @@
 #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);
+
+	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(mnt, CLONE_NEWNS);
+	JOIN_NS(ve, CLONE_NEWVE);
+	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 original one).
+ */
+static inline int prepare_namespaces(struct namespace_ctx *ctx, int flags,
+	int cgv2_fd, int ctid)
+{
+	struct namespace_ctx tmp_ctx;
+
+	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__);
+			return -1;
+		}
+	}
+
+	if (unshare(flags) < 0) {
+		fprintf(stderr, "%s: failed to unshare, flags %x: %s\n",
+			__func__, flags, strerror(errno));
+		return -1;
+	}
+
+	if (save_namespace_fds(ctx, flags)) {
+		fprintf(stderr, "%s: failed to save new namespaces, flags %x\n",
+			__func__, flags);
+		return -1;
+	}
+
+	if (flags & CLONE_NEWVE) {
+		if (enter_cgroup(cgv2_fd, 0) < 0) {
+			fprintf(stderr, "%s: failed to restore root cgroup\n", __func__);
+			return -1;
+		}
+	}
+
+	if (enter_namespaces(&tmp_ctx, flags) < 0) {
+		fprintf(stderr, "%s: failed to restore old namespaces, flags %x\n",
+			__func__, flags);
+		return -1;
+	}
+
+	close_namespace_fds(&tmp_ctx);
+	return 0;
+}
+
 static inline int write_file_at(int dirfd, const char *path, const char *val)
 {
 	int fd, ret;

-- 
2.47.1

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

^ permalink raw reply	[relevance 6%]

* [Devel] [PATCH VZ10 3/3] ve_namespace: Allow entering non-running VE namespace
    2026-09-07 19:14  6% ` [Devel] [PATCH VZ10 1/3] selftests/ve: Add namespace management helpers Vladimir Riabchun
  2026-09-07 19:14  6% ` [Devel] [PATCH VZ10 2/3] selftests/ve: Add more helpers Vladimir Riabchun
@ 2026-09-07 19:14  4% ` Vladimir Riabchun
  2026-09-08  9:12  0%   ` Pavel Tikhomirov
  2 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-09-07 19:14 UTC (permalink / raw)
  To: Konstantin Khorenko; +Cc: devel

This doesn't bring a lot of value and makes tests more complex.
To enter VE namespace we need to actually start VE.

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.

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

Feature: ve selftests
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@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 3dbe0dde2645..2ddec9ef312f 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("Switching to a non-running ve %s.\n"
+			"This should happen in kselftests only.\n",
+			ve_name(ve_ns->ve));
 
 	/*
 	 * Don't need to put_ve_ns(nsset->ve_ns) here,

-- 
2.47.1

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

^ permalink raw reply	[relevance 4%]

* Re: [QEMU HCI-8.0 PATCH v2 4/5] vhost-blk: watch the device for resize events
  2026-09-04 15:33  6%   ` Andrey Drobyshev
@ 2026-09-08  0:31  0%     ` Andrey Zhadchenko
  0 siblings, 0 replies; 200+ results
From: Andrey Zhadchenko @ 2026-09-08  0:31 UTC (permalink / raw)
  To: Andrey Drobyshev; +Cc: svt-core, den



On 9/4/26 17:33, Andrey Drobyshev wrote:
>> Resize was tied to block node, which we removed a few patches ago.
>> Luckily we can make resize automated: receive an uevent socket
>> from the management layer via the new "ueventfd" property (e.g. a
>> /dev/fdset/N path), watch it for relevant netlink messages and
>> call virtio_notify_config() if we detect a capacity change.
>>
>> The socket is set up (possibly with filter) by management layer.
>> We only need to check that it is netlink and do some message
>> filtering.
>>
>> When the property is not set, capacity changes are not detected.
> 
> So libvirt must ALWAYS set it, and we fail in .realize() if it's not
> set.  Correct?  Let's mention it.

This is an optional field. If it is not set, nothing happens:
vhost_blk_uevent_attach() returns true and realize goes on.

> 
>>
>> https://virtuozzo.atlassian.net/browse/VSTOR-143437
>> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
>>
>> diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
>> index 04a7013e521..7cb842a859e 100644
>> --- a/hw/block/vhost-blk.c
>> +++ b/hw/block/vhost-blk.c
>> @@ -10,7 +10,9 @@
>>   
>>   #include "qemu/osdep.h"
>>   #include "qapi/error.h"
>> +#include "qemu/cutils.h"
>>   #include "qemu/error-report.h"
>> +#include "qemu/main-loop.h"
>>   #include "qom/object.h"
>>   #include "hw/qdev-core.h"
>>   #include "hw/boards.h"
>> @@ -25,6 +27,7 @@
>>   #include "linux-headers/linux/vhost.h"
>>   #include <sys/ioctl.h>
>>   #include <linux/fs.h>
>> +#include <linux/netlink.h>
>>   #include "system/runstate.h"
>>   
>>   static int vhost_blk_start(VirtIODevice *vdev)
>> @@ -228,6 +231,173 @@ static int vhost_blk_update_size(VHostBlk *s, bool *changed, Error **errp)
>>       return 0;
>>   }
>>   
>> +static void vhost_blk_resize_bh(void *opaque)
>> +{
>> +    VHostBlk *s = opaque;
>> +    Error *local_err = NULL;
>> +    bool changed;
>> +
>> +    if (vhost_blk_update_size(s, &changed, &local_err) < 0) {
>> +        error_report_err(local_err);
>> +        return;
>> +    }
>> +
>> +    if (changed) {
>> +        virtio_notify_config(VIRTIO_DEVICE(s));
>> +    }
>> +}
>> +
>> +/*
>> + * The uevent socket is created, bound and filtered by the management
>> + * layer and passed to us via the "ueventfd" property.
>> + */
>> +static void vhost_blk_uevent_read(void *opaque)
>> +{
>> +    VHostBlk *s = opaque;
>> +    char buffer[64 * 1024 + 1];
> 
> include/linux/kobject.h
> 33:#define UEVENT_BUFFER_SIZE           2048    /* buffer for the variables */
> 
> Looks like 64K is overkill, maybe 4K?
> 
>> +
>> +    for (;;) {
>> +        struct sockaddr_nl source;
>> +        socklen_t source_len = sizeof(source);
>> +        uint64_t event_major = UINT64_MAX;
>> +        uint64_t event_minor = UINT64_MAX;
>> +        bool action_change = false;
>> +        bool subsystem_block = false;
>> +        bool resize = false;
>> +        char *field;
>> +        char *end;
>> +        ssize_t len;
>> +
>> +        memset(&source, 0, sizeof(source));
>> +        len = recvfrom(s->uevent_fd, buffer, sizeof(buffer) - 1,
>> +                       MSG_DONTWAIT, (struct sockaddr *)&source, &source_len);
>> +        if (len < 0) {
>> +            if (errno == EINTR) {
>> +                continue;
>> +            }
>> +            if (errno == ENOBUFS) {
>> +                /* Some events may be dropped, just re-check */
>> +                qemu_bh_schedule(s->resize_bh);
>> +                continue;
>> +            }
>> +            if (errno != EAGAIN && errno != EWOULDBLOCK) {
> 
> Should ENOMEM go here to?  And if it's an error that breaks the socket,
> like EBADF or smth else - maybe spit the error and detach the handler?
> What's the point of keeping it afterwards?

ENOMEM is expected only for recvmsg.

> 
>> +                error_report("vhost-blk: unable to receive uevent: %s",
>> +                             strerror(errno));
>> +            }
>> +            return;
>> +        }
>> +
>> +        if (source.nl_family != AF_NETLINK || source.nl_pid != 0) {
>> +            continue;
>> +        }
>> +
>> +        buffer[len] = '\0';
>> +        field = buffer;
>> +        end = buffer + len;
>> +        while (field < end) {
>> +            size_t field_len = strnlen(field, end - field);
>> +
>> +            if (!strcmp(field, "ACTION=change")) {
>> +                action_change = true;
>> +            } else if (!strcmp(field, "SUBSYSTEM=block")) {
>> +                subsystem_block = true;
>> +            } else if (!strcmp(field, "RESIZE=1")) {
>> +                resize = true;
>> +            } else if (g_str_has_prefix(field, "MAJOR=")) {
>> +                uint64_t value;
>> +
>> +                if (!qemu_strtou64(field + strlen("MAJOR="), NULL, 10,
>> +                                   &value)) {
>> +                    event_major = value;
>> +                }
>> +            } else if (g_str_has_prefix(field, "MINOR=")) {
>> +                uint64_t value;
>> +
>> +                if (!qemu_strtou64(field + strlen("MINOR="), NULL, 10,
>> +                                   &value)) {
>> +                    event_minor = value;
>> +                }
>> +            }
>> +
>> +            if (field_len == end - field) {
>> +                break;
>> +            }
>> +            field += field_len + 1;
>> +        }
>> +
>> +        if (action_change && subsystem_block && resize &&
>> +            event_major == major(s->backend_rdev) &&
>> +            event_minor == minor(s->backend_rdev)) {
>> +            qemu_bh_schedule(s->resize_bh);
>> +        }
>> +    }
>> +}
>> +
>> +static bool vhost_blk_uevent_check(int fd, const char *src, Error **errp)
>> +{
>> +    socklen_t optlen;
>> +    int domain;
>> +    int protocol;
>> +
>> +    optlen = sizeof(domain);
>> +    if (getsockopt(fd, SOL_SOCKET, SO_DOMAIN, &domain, &optlen) < 0) {
>> +        error_setg_errno(errp, errno, "vhost-blk: '%s' is not a socket", src);
>> +        return false;
>> +    }
>> +
>> +    optlen = sizeof(protocol);
>> +    if (getsockopt(fd, SOL_SOCKET, SO_PROTOCOL, &protocol, &optlen) < 0) {
>> +        error_setg_errno(errp, errno,
>> +                         "vhost-blk: unable to get protocol of '%s'", src);
>> +        return false;
>> +    }
>> +
>> +    if (domain != AF_NETLINK || protocol != NETLINK_KOBJECT_UEVENT) {
>> +        error_setg(errp,
>> +                   "vhost-blk: '%s' is not a NETLINK_KOBJECT_UEVENT socket",
>> +                   src);
>> +        return false;
>> +    }
> 
> How about also checking nl_groups? Should be 1 for multicast.  I.e.
> 
>    if (getsockname(fd, (struct sockaddr *)&addr, &addrlen) < 0) {
>        error_setg_errno(...);
>        return false;
>    }
>    if (addr.nl_groups != 1) {
>        error_setg(errp, "vhost-blk: '%s' is not subscribed to kernel uevents",
>                   src);
>        return false;
>    }
> 
>> +
>> +    return true;
>> +}
>> +
>> +static bool vhost_blk_uevent_attach(VHostBlk *s, Error **errp)
>> +{
>> +    if (!s->conf.ueventfd) {
>> +        return true;
>> +    }
>> +
>> +    s->uevent_fd = qemu_open(s->conf.ueventfd, O_RDWR, errp);
> 
> For conf values monitor_fd_param() is usually used, so how about
> 
>    s->uevent_fd = monitor_fd_param(monitor_cur(), s->conf.ueventfd, errp);
> 
> Andrey
> 


^ permalink raw reply	[relevance 0%]

* [QEMU HCI-8.0 PATCH v3 1/5] vhost-blk: improve vhost_blk_device_realize cleanup
  @ 2026-09-08  1:04 21% ` Andrey Zhadchenko
  2026-09-08  1:04 23% ` [QEMU HCI-8.0 PATCH v3 2/5] vhost-blk: change backend setup Andrey Zhadchenko
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 200+ results
From: Andrey Zhadchenko @ 2026-09-08  1:04 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

vhost_dev_init() does a lot of unexpected things:
 - closes the backend fd on failure
 - memsets dev as 0 (thus clearing dev->vqs)

Cleanup properly: don't double-close vhostfd, factor out vqs
so we do not leak it during cleanup.

Fixes: 0ff3cb5ff2 ("block: add vhost-blk backend")
https://virtuozzo.atlassian.net/browse/VSTOR-143437
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
v2:
 - old patch dropped

v3:
 - extend the double-close fix to preserve and free vqs after
vhost_dev_init() clears dev

 hw/block/vhost-blk.c | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index c52851fcf8..20c2f41691 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -237,6 +237,7 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
     VirtIODevice *vdev = VIRTIO_DEVICE(dev);
     VHostBlk *s = VHOST_BLK(vdev);
     VhostBlkConf *conf = &s->conf;
+    struct vhost_virtqueue *vqs;
     int i, ret;
 
     if (!conf->conf.blk) {
@@ -289,7 +290,8 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
 
     s->dev.nvqs = conf->num_queues;
     s->dev.max_queues = conf->num_queues;
-    s->dev.vqs = g_new0(struct vhost_virtqueue, s->dev.nvqs);
+    vqs = g_new0(struct vhost_virtqueue, s->dev.nvqs);
+    s->dev.vqs = vqs;
     s->dev.vq_index = 0;
 
     virtio_init(vdev, VIRTIO_ID_BLOCK, sizeof(struct virtio_blk_config));
@@ -318,6 +320,8 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
     if (ret < 0) {
         error_setg(errp, "vhost-blk: vhost initialization failed: %s",
                 strerror(-ret));
+        /* vhost_dev_init() closes vhostfd on failure */
+        s->vhostfd = -1;
         goto cleanup;
     }
 
@@ -327,8 +331,11 @@ cleanup:
     if (s->mighand) {
             qemu_del_vm_change_state_handler(s->mighand);
     }
-    g_free(s->dev.vqs);
-    close(s->vhostfd);
+    g_free(vqs);
+    if (s->vhostfd >= 0) {
+        close(s->vhostfd);
+        s->vhostfd = -1;
+    }
     for (i = 0; i < conf->num_queues; i++) {
         virtio_del_queue(vdev, i);
     }
@@ -470,6 +477,7 @@ static void vhost_blk_instance_init(Object *obj)
 {
     VHostBlk *s = VHOST_BLK(obj);
 
+    s->vhostfd = -1;
     device_add_bootindex_property(obj, &s->conf.conf.bootindex,
                                   "bootindex", "/disk@0,0",
                                   DEVICE(obj));
-- 
2.43.5


^ permalink raw reply	[relevance 21%]

* [QEMU HCI-8.0 PATCH v3 3/5] vhost-blk: add read-only flag
    2026-09-08  1:04 21% ` [QEMU HCI-8.0 PATCH v3 1/5] vhost-blk: improve vhost_blk_device_realize cleanup Andrey Zhadchenko
  2026-09-08  1:04 23% ` [QEMU HCI-8.0 PATCH v3 2/5] vhost-blk: change backend setup Andrey Zhadchenko
@ 2026-09-08  1:04 27% ` Andrey Zhadchenko
  2026-09-08  1:04 19% ` [QEMU HCI-8.0 PATCH v3 4/5] vhost-blk: watch the device for resize events Andrey Zhadchenko
  2026-09-08  1:04 21% ` [QEMU HCI-8.0 PATCH v3 5/5] vhost-blk: preserve the uevent socket across cpr-exec Andrey Zhadchenko
  4 siblings, 0 replies; 200+ results
From: Andrey Zhadchenko @ 2026-09-08  1:04 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

Also compare BLKROGET with the selected mode and reject r/w if
needed.

https://virtuozzo.atlassian.net/browse/VSTOR-143437
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
v2:
 - check BLKROGET only if rw was requested like posix_file

 hw/block/vhost-blk.c          | 25 ++++++++++++++++++++++++-
 include/hw/virtio/vhost-blk.h |  1 +
 2 files changed, 25 insertions(+), 1 deletion(-)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index d6681235e5..797743b2c1 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -232,8 +232,9 @@ static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
     ERRP_GUARD();
     BlockConf *conf = &s->conf.conf;
     struct stat st;
+    int open_flags = s->conf.readonly ? O_RDONLY : O_RDWR;
 
-    s->backend_fd = qemu_open(s->conf.devpath, O_RDWR, errp);
+    s->backend_fd = qemu_open(s->conf.devpath, open_flags, errp);
     if (s->backend_fd < 0) {
         error_prepend(errp, "vhost-blk: unable to open backend: ");
         return false;
@@ -251,6 +252,23 @@ static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
         goto fail;
     }
 
+    if (!s->conf.readonly) {
+        int readonly;
+
+        if (ioctl(s->backend_fd, BLKROGET, &readonly) < 0) {
+            error_setg_errno(errp, errno,
+                             "vhost-blk: unable to get read-only status of "
+                             "'%s'", s->conf.devpath);
+            goto fail;
+        }
+
+        if (readonly) {
+            error_setg_errno(errp, EROFS, "vhost-blk: '%s' is not writable",
+                             s->conf.devpath);
+            goto fail;
+        }
+    }
+
     vhost_blk_update_size(s, errp);
     if (*errp) {
         goto fail;
@@ -409,6 +427,10 @@ static uint64_t vhost_blk_get_features(VirtIODevice *vdev,
 
     virtio_add_feature(&features, VIRTIO_F_VERSION_1);
 
+    if (s->conf.readonly) {
+        virtio_add_feature(&features, VIRTIO_BLK_F_RO);
+    }
+
     if (s->conf.num_queues > 1) {
         virtio_add_feature(&features, VIRTIO_BLK_F_MQ);
     }
@@ -456,6 +478,7 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
 
 static const Property vhost_blk_properties[] = {
     DEFINE_PROP_STRING("devpath", VHostBlk, conf.devpath),
+    DEFINE_PROP_BOOL("read-only", VHostBlk, conf.readonly, false),
     DEFINE_PROP_UINT16("num-queues", VHostBlk, conf.num_queues,
                        VHOST_BLK_AUTO_NUM_QUEUES),
     DEFINE_PROP_UINT16("queue-size", VHostBlk, conf.queue_size, 256),
diff --git a/include/hw/virtio/vhost-blk.h b/include/hw/virtio/vhost-blk.h
index c194b421d9..c6646f5845 100644
--- a/include/hw/virtio/vhost-blk.h
+++ b/include/hw/virtio/vhost-blk.h
@@ -25,6 +25,7 @@
 typedef struct VhostBlkConf {
     BlockConf conf;
     char *devpath;
+    bool readonly;
     uint16_t num_queues;
     uint16_t queue_size;
     uint16_t num_threads;
-- 
2.43.5


^ permalink raw reply	[relevance 27%]

* [QEMU HCI-8.0 PATCH v3 4/5] vhost-blk: watch the device for resize events
                     ` (2 preceding siblings ...)
  2026-09-08  1:04 27% ` [QEMU HCI-8.0 PATCH v3 3/5] vhost-blk: add read-only flag Andrey Zhadchenko
@ 2026-09-08  1:04 19% ` Andrey Zhadchenko
  2026-09-08  1:04 21% ` [QEMU HCI-8.0 PATCH v3 5/5] vhost-blk: preserve the uevent socket across cpr-exec Andrey Zhadchenko
  4 siblings, 0 replies; 200+ results
From: Andrey Zhadchenko @ 2026-09-08  1:04 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

Resize was tied to block node, which we removed a few patches ago.
Luckily we can make resize automated: receive an uevent socket
from the management layer via the new "ueventfd" property (a
monitor fd name passed with the 'getfd' QMP command), watch it for
relevant netlink messages and call virtio_notify_config() if we
detect a capacity change.

The socket is set up (possibly with filter) by management layer.
We only need to check that it is netlink, subscribed to kernel
uevents, and do some message filtering.

When the property is not set, capacity changes are not detected.

https://virtuozzo.atlassian.net/browse/VSTOR-143437
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
v2:
 - rework patch a bit: now we expect opened and filtered fd from
libvirt.

v3:
 - accept a monitor fd name passed with getfd instead of a
/dev/fdset path
 - verify that the socket is subscribed to kernel uevents
 - detach the socket after a permanent receive error
 - change buffer capacity to 4KiB

 hw/block/vhost-blk.c          | 200 ++++++++++++++++++++++++++++++++++
 include/hw/virtio/vhost-blk.h |   4 +
 2 files changed, 204 insertions(+)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index 797743b2c1..a69bdcb551 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -10,7 +10,10 @@
 
 #include "qemu/osdep.h"
 #include "qapi/error.h"
+#include "qemu/cutils.h"
 #include "qemu/error-report.h"
+#include "qemu/main-loop.h"
+#include "monitor/monitor.h"
 #include "qom/object.h"
 #include "hw/qdev-core.h"
 #include "hw/boards.h"
@@ -25,6 +28,7 @@
 #include "linux-headers/linux/vhost.h"
 #include <sys/ioctl.h>
 #include <linux/fs.h>
+#include <linux/netlink.h>
 #include "system/runstate.h"
 
 static int vhost_blk_start(VirtIODevice *vdev)
@@ -227,6 +231,191 @@ static bool vhost_blk_update_size(VHostBlk *s, Error **errp)
     return changed;
 }
 
+static void vhost_blk_resize_bh(void *opaque)
+{
+    VHostBlk *s = opaque;
+    Error *local_err = NULL;
+    bool changed;
+
+    changed = vhost_blk_update_size(s, &local_err);
+    if (local_err) {
+        error_report_err(local_err);
+        return;
+    }
+
+    if (changed) {
+        virtio_notify_config(VIRTIO_DEVICE(s));
+    }
+}
+
+static void vhost_blk_uevent_detach(VHostBlk *s)
+{
+    if (s->uevent_fd < 0) {
+        return;
+    }
+
+    qemu_set_fd_handler(s->uevent_fd, NULL, NULL, NULL);
+    qemu_bh_delete(s->resize_bh);
+    s->resize_bh = NULL;
+    qemu_close(s->uevent_fd);
+    s->uevent_fd = -1;
+}
+
+/*
+ * The uevent socket is created, bound and filtered by the management
+ * layer and passed to us via the "ueventfd" property.
+ */
+static void vhost_blk_uevent_read(void *opaque)
+{
+    VHostBlk *s = opaque;
+    char buffer[4096];
+
+    for (;;) {
+        struct sockaddr_nl source;
+        socklen_t source_len = sizeof(source);
+        uint64_t event_major = UINT64_MAX;
+        uint64_t event_minor = UINT64_MAX;
+        bool action_change = false;
+        bool subsystem_block = false;
+        bool resize = false;
+        char *field;
+        char *end;
+        ssize_t len;
+
+        memset(&source, 0, sizeof(source));
+        len = recvfrom(s->uevent_fd, buffer, sizeof(buffer) - 1,
+                       MSG_DONTWAIT, (struct sockaddr *)&source, &source_len);
+        if (len < 0) {
+            if (errno == EINTR) {
+                continue;
+            }
+            if (errno == ENOBUFS) {
+                /* Some events may be dropped, just re-check */
+                qemu_bh_schedule(s->resize_bh);
+                continue;
+            }
+            if (errno != EAGAIN && errno != EWOULDBLOCK) {
+                error_report("vhost-blk: detaching uevent after the error: %s",
+                             strerror(errno));
+                vhost_blk_uevent_detach(s);
+            }
+            return;
+        }
+
+        if (source.nl_family != AF_NETLINK || source.nl_pid != 0) {
+            continue;
+        }
+
+        buffer[len] = '\0';
+        field = buffer;
+        end = buffer + len;
+        while (field < end) {
+            size_t field_len = strnlen(field, end - field);
+
+            if (!strcmp(field, "ACTION=change")) {
+                action_change = true;
+            } else if (!strcmp(field, "SUBSYSTEM=block")) {
+                subsystem_block = true;
+            } else if (!strcmp(field, "RESIZE=1")) {
+                resize = true;
+            } else if (g_str_has_prefix(field, "MAJOR=")) {
+                uint64_t value;
+
+                if (!qemu_strtou64(field + strlen("MAJOR="), NULL, 10,
+                                   &value)) {
+                    event_major = value;
+                }
+            } else if (g_str_has_prefix(field, "MINOR=")) {
+                uint64_t value;
+
+                if (!qemu_strtou64(field + strlen("MINOR="), NULL, 10,
+                                   &value)) {
+                    event_minor = value;
+                }
+            }
+
+            if (field_len == end - field) {
+                break;
+            }
+            field += field_len + 1;
+        }
+
+        if (action_change && subsystem_block && resize &&
+            event_major == major(s->backend_rdev) &&
+            event_minor == minor(s->backend_rdev)) {
+            qemu_bh_schedule(s->resize_bh);
+        }
+    }
+}
+
+static bool vhost_blk_uevent_check(int fd, const char *src, Error **errp)
+{
+    struct sockaddr_nl addr;
+    socklen_t addrlen;
+    socklen_t optlen;
+    int domain;
+    int protocol;
+
+    optlen = sizeof(domain);
+    if (getsockopt(fd, SOL_SOCKET, SO_DOMAIN, &domain, &optlen) < 0) {
+        error_setg_errno(errp, errno, "vhost-blk: '%s' is not a socket", src);
+        return false;
+    }
+
+    optlen = sizeof(protocol);
+    if (getsockopt(fd, SOL_SOCKET, SO_PROTOCOL, &protocol, &optlen) < 0) {
+        error_setg_errno(errp, errno,
+                         "vhost-blk: unable to get protocol of '%s'", src);
+        return false;
+    }
+
+    if (domain != AF_NETLINK || protocol != NETLINK_KOBJECT_UEVENT) {
+        error_setg(errp,
+                   "vhost-blk: '%s' is not a NETLINK_KOBJECT_UEVENT socket",
+                   src);
+        return false;
+    }
+
+    addrlen = sizeof(addr);
+    if (getsockname(fd, (struct sockaddr *)&addr, &addrlen) < 0) {
+        error_setg_errno(errp, errno,
+                         "vhost-blk: unable to get address of '%s'", src);
+        return false;
+    }
+
+    if (addr.nl_groups != 1) {
+        error_setg(errp,
+                   "vhost-blk: '%s' is not subscribed to kernel uevents",
+                   src);
+        return false;
+    }
+
+    return true;
+}
+
+static bool vhost_blk_uevent_attach(VHostBlk *s, Error **errp)
+{
+    if (!s->conf.ueventfd) {
+        return true;
+    }
+
+    s->uevent_fd = monitor_fd_param(monitor_cur(), s->conf.ueventfd, errp);
+    if (s->uevent_fd < 0) {
+        error_prepend(errp, "vhost-blk: unable to obtain uevent socket: ");
+        return false;
+    }
+
+    if (!vhost_blk_uevent_check(s->uevent_fd, s->conf.ueventfd, errp)) {
+        qemu_close(s->uevent_fd);
+        s->uevent_fd = -1;
+        return false;
+    }
+
+    s->resize_bh = qemu_bh_new(vhost_blk_resize_bh, s);
+    qemu_set_fd_handler(s->uevent_fd, vhost_blk_uevent_read, NULL, s);
+    return true;
+}
+
 static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
 {
     ERRP_GUARD();
@@ -251,6 +440,7 @@ static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
                    s->conf.devpath);
         goto fail;
     }
+    s->backend_rdev = st.st_rdev;
 
     if (!s->conf.readonly) {
         int readonly;
@@ -326,6 +516,12 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
         return;
     }
 
+    if (!vhost_blk_uevent_attach(s, errp)) {
+        qemu_close(s->backend_fd);
+        s->backend_fd = -1;
+        return;
+    }
+
     s->dev.nvqs = conf->num_queues;
     s->dev.max_queues = conf->num_queues;
     vqs = g_new0(struct vhost_virtqueue, s->dev.nvqs);
@@ -376,6 +572,7 @@ cleanup:
         qemu_close(s->backend_fd);
         s->backend_fd = -1;
     }
+    vhost_blk_uevent_detach(s);
     for (i = 0; i < conf->num_queues; i++) {
         virtio_del_queue(vdev, i);
     }
@@ -388,6 +585,7 @@ static void vhost_blk_device_unrealize(DeviceState *dev)
     VirtIODevice *vdev = VIRTIO_DEVICE(dev);
     VHostBlk *s = VHOST_BLK(dev);
 
+    vhost_blk_uevent_detach(s);
     qemu_del_vm_change_state_handler(s->mighand);
     vhost_blk_set_status(vdev, 0);
     vhost_dev_cleanup(&s->dev);
@@ -478,6 +676,7 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
 
 static const Property vhost_blk_properties[] = {
     DEFINE_PROP_STRING("devpath", VHostBlk, conf.devpath),
+    DEFINE_PROP_STRING("ueventfd", VHostBlk, conf.ueventfd),
     DEFINE_PROP_BOOL("read-only", VHostBlk, conf.readonly, false),
     DEFINE_PROP_UINT16("num-queues", VHostBlk, conf.num_queues,
                        VHOST_BLK_AUTO_NUM_QUEUES),
@@ -525,6 +724,7 @@ static void vhost_blk_instance_init(Object *obj)
 
     s->vhostfd = -1;
     s->backend_fd = -1;
+    s->uevent_fd = -1;
     device_add_bootindex_property(obj, &s->conf.conf.bootindex,
                                   "bootindex", "/disk@0,0",
                                   DEVICE(obj));
diff --git a/include/hw/virtio/vhost-blk.h b/include/hw/virtio/vhost-blk.h
index c6646f5845..e3ad92bc6e 100644
--- a/include/hw/virtio/vhost-blk.h
+++ b/include/hw/virtio/vhost-blk.h
@@ -25,6 +25,7 @@
 typedef struct VhostBlkConf {
     BlockConf conf;
     char *devpath;
+    char *ueventfd;
     bool readonly;
     uint16_t num_queues;
     uint16_t queue_size;
@@ -40,9 +41,12 @@ typedef struct VHostBlk {
     uint64_t decided_features;
     int vhostfd;
     int backend_fd;
+    int uevent_fd;
     struct vhost_dev dev;
     bool vhost_started;
     uint64_t length;
+    uint64_t backend_rdev;
+    QEMUBH *resize_bh;
 } VHostBlk;
 
 #endif
-- 
2.43.5


^ permalink raw reply	[relevance 19%]

* [QEMU HCI-8.0 PATCH v3 2/5] vhost-blk: change backend setup
    2026-09-08  1:04 21% ` [QEMU HCI-8.0 PATCH v3 1/5] vhost-blk: improve vhost_blk_device_realize cleanup Andrey Zhadchenko
@ 2026-09-08  1:04 23% ` Andrey Zhadchenko
  2026-09-08 11:11  0%   ` Andrey Drobyshev
  2026-09-08  1:04 27% ` [QEMU HCI-8.0 PATCH v3 3/5] vhost-blk: add read-only flag Andrey Zhadchenko
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 200+ results
From: Andrey Zhadchenko @ 2026-09-08  1:04 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

Previously we used very ugly and incapsulation-breaking assignment
fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
It is wrong in a many ways, so let's rework this.

Patch changes default `drive` to new `devpath` option so device fd
is managed by vhost-blk itself. Unfortunately this way we need a
bit more preparational work: finding out disk length, etc. Don't
be too broad and just do the minimal work.
Also drop generic block device properties along with the block
node: the kernel module does all IO in terms of 512 sectors, so
simply report 512 byte logical/physical block size to the guest.
Existing vhost-blk setups will break, but it is expected.

We also lose resize, as this is tied to the block node, which is
now have no place in the setup. We will add this in the next
patches as well as RO mode.

https://virtuozzo.atlassian.net/browse/VSTOR-143437
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
v2:
 - moved double close fix to 'do not double close vhostfd' patch
 - dropped DEFINE_BLOCK_PROPERTIES_BASE exposition to the outside. Let's
just use defaults for now. We can later check if tweaking this values
makes any difference and add them back.
 - change vhost_blk_update_size() to return zero or error and move 'changed'
to a separate argument
 - changed lseek to BLKGETSIZE64 (we already use BLKROGET anyway)

v3:
 - make vhost_blk_update_size() return the changed flag directly
 - use ERRP_GUARD() for reliable error propagation

 hw/block/vhost-blk.c          | 124 ++++++++++++++++++++--------------
 include/hw/virtio/vhost-blk.h |   5 +-
 2 files changed, 77 insertions(+), 52 deletions(-)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index 20c2f41691..d6681235e5 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -25,22 +25,17 @@
 #include "linux-headers/linux/vhost.h"
 #include <sys/ioctl.h>
 #include <linux/fs.h>
-#include "include/block/block_int-common.h"
 #include "system/runstate.h"
 
 static int vhost_blk_start(VirtIODevice *vdev)
 {
     VHostBlk *s = VHOST_BLK(vdev);
     struct vhost_vring_file backend;
-    int ret, i, nworkers, *fd;
+    int ret, i, nworkers;
     BusState *qbus = BUS(qdev_get_parent_bus(DEVICE(vdev)));
     VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
     char serial[VIRTIO_BLK_ID_BYTES] = {0};
 
-    bdrv_graph_rdlock_main_loop();
-    fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
-    bdrv_graph_rdunlock_main_loop();
-
     if (!k->set_guest_notifiers) {
         error_report("vhost-blk: binding does not support guest notifiers");
         return -ENOSYS;
@@ -92,7 +87,7 @@ static int vhost_blk_start(VirtIODevice *vdev)
 
     memset(&backend, 0, sizeof(backend));
     backend.index = 0;
-    backend.fd = *fd;
+    backend.fd = s->backend_fd;
     if (ioctl(s->vhostfd, VHOST_BLK_SET_BACKEND, &backend)) {
         error_report("vhost-blk: unable to set backend");
         ret = -errno;
@@ -208,29 +203,69 @@ static void vhost_blk_vm_state(void *opaque, bool running, RunState state)
     }
 }
 
-static void vhost_blk_resize_cb(void *opaque)
+static bool vhost_blk_update_size(VHostBlk *s, Error **errp)
 {
-    VirtIODevice *vdev = opaque;
+    BlockConf *conf = &s->conf.conf;
+    uint64_t length;
+    bool changed;
+
+    if (ioctl(s->backend_fd, BLKGETSIZE64, &length) < 0) {
+        error_setg_errno(errp, errno,
+                         "vhost-blk: unable to determine size of '%s'",
+                         s->conf.devpath);
+        return false;
+    }
 
-    assert(qemu_get_current_aio_context() == qemu_get_aio_context());
-    virtio_notify_config(vdev);
+    changed = s->length != length;
+    s->length = length;
+    conf->heads = 16;
+    conf->secs = 63;
+    conf->cyls = s->length / BDRV_SECTOR_SIZE /
+                 (conf->heads * conf->secs);
+    conf->cyls = MIN(MAX(conf->cyls, 2U), 16383U);
+
+    return changed;
 }
 
-static void vhost_blk_resize(void *opaque)
+static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
 {
-    VirtIODevice *vdev = VIRTIO_DEVICE(opaque);
+    ERRP_GUARD();
+    BlockConf *conf = &s->conf.conf;
+    struct stat st;
 
-    /*
-     * virtio_notify_config() needs to acquire the global mutex,
-     * so it can't be called from an iothread. Instead, schedule
-     * it to be run in the main context BH.
-     */
-    aio_bh_schedule_oneshot(qemu_get_aio_context(), vhost_blk_resize_cb, vdev);
-}
+    s->backend_fd = qemu_open(s->conf.devpath, O_RDWR, errp);
+    if (s->backend_fd < 0) {
+        error_prepend(errp, "vhost-blk: unable to open backend: ");
+        return false;
+    }
 
-static const BlockDevOps vhost_blk_block_ops = {
-    .resize_cb     = vhost_blk_resize,
-};
+    if (fstat(s->backend_fd, &st) < 0) {
+        error_setg_errno(errp, errno, "vhost-blk: unable to stat '%s'",
+                         s->conf.devpath);
+        goto fail;
+    }
+
+    if (!S_ISBLK(st.st_mode)) {
+        error_setg(errp, "vhost-blk: '%s' is not a block device",
+                   s->conf.devpath);
+        goto fail;
+    }
+
+    vhost_blk_update_size(s, errp);
+    if (*errp) {
+        goto fail;
+    }
+
+    conf->logical_block_size = BDRV_SECTOR_SIZE;
+    conf->physical_block_size = BDRV_SECTOR_SIZE;
+
+    return true;
+
+fail:
+    qemu_close(s->backend_fd);
+    s->backend_fd = -1;
+    return false;
+}
 
 static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
 {
@@ -240,13 +275,8 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
     struct vhost_virtqueue *vqs;
     int i, ret;
 
-    if (!conf->conf.blk) {
-        error_setg(errp, "vhost-blk: drive property not set");
-        return;
-    }
-
-    if (!blk_is_inserted(conf->conf.blk)) {
-        error_setg(errp, "vhost-blk: device needs media, but drive is empty");
+    if (!conf->devpath) {
+        error_setg(errp, "vhost-blk: devpath property must be set");
         return;
     }
 
@@ -274,17 +304,7 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
         return;
     }
 
-    if (!blkconf_apply_backend_options(&conf->conf,
-                                       !blk_supports_write_perm(conf->conf.blk),
-                                       true, errp)) {
-        return;
-    }
-
-    if (!blkconf_geometry(&conf->conf, NULL, 65535, 255, 255, errp)) {
-        return;
-    }
-
-    if (!blkconf_blocksizes(&conf->conf, errp)) {
+    if (!vhost_blk_open_backend(s, errp)) {
         return;
     }
 
@@ -313,8 +333,6 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
         goto cleanup;
     }
 
-    blk_set_dev_ops(s->conf.conf.blk, &vhost_blk_block_ops, s);
-
     ret = vhost_dev_init(&s->dev, (void *)((size_t)s->vhostfd),
                          VHOST_BACKEND_TYPE_KERNEL, 0, NULL);
     if (ret < 0) {
@@ -336,6 +354,10 @@ cleanup:
         close(s->vhostfd);
         s->vhostfd = -1;
     }
+    if (s->backend_fd >= 0) {
+        qemu_close(s->backend_fd);
+        s->backend_fd = -1;
+    }
     for (i = 0; i < conf->num_queues; i++) {
         virtio_del_queue(vdev, i);
     }
@@ -351,6 +373,10 @@ static void vhost_blk_device_unrealize(DeviceState *dev)
     qemu_del_vm_change_state_handler(s->mighand);
     vhost_blk_set_status(vdev, 0);
     vhost_dev_cleanup(&s->dev);
+    if (s->backend_fd >= 0) {
+        qemu_close(s->backend_fd);
+        s->backend_fd = -1;
+    }
     g_free(s->dev.vqs);
     virtio_cleanup(vdev);
 }
@@ -383,10 +409,6 @@ static uint64_t vhost_blk_get_features(VirtIODevice *vdev,
 
     virtio_add_feature(&features, VIRTIO_F_VERSION_1);
 
-    if (!blk_is_writable(s->conf.conf.blk)) {
-        virtio_add_feature(&features, VIRTIO_BLK_F_RO);
-    }
-
     if (s->conf.num_queues > 1) {
         virtio_add_feature(&features, VIRTIO_BLK_F_MQ);
     }
@@ -405,7 +427,9 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
     int64_t length;
     int blk_size = conf->logical_block_size;
 
-    blk_get_geometry(s->conf.conf.blk, &capacity);
+    length = s->length;
+    capacity = length / BDRV_SECTOR_SIZE;
+
     memset(&blkcfg, 0, sizeof(blkcfg));
     virtio_stq_p(vdev, &blkcfg.capacity, capacity);
     virtio_stl_p(vdev, &blkcfg.seg_max, s->conf.queue_size - 2);
@@ -413,7 +437,6 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
     virtio_stl_p(vdev, &blkcfg.blk_size, blk_size);
     blkcfg.geometry.heads = conf->heads;
 
-    length = blk_getlength(s->conf.conf.blk);
     if (length > 0 && length / conf->heads / conf->secs % blk_size) {
         unsigned short mask;
 
@@ -432,7 +455,7 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
 }
 
 static const Property vhost_blk_properties[] = {
-    DEFINE_BLOCK_PROPERTIES(VHostBlk, conf.conf),
+    DEFINE_PROP_STRING("devpath", VHostBlk, conf.devpath),
     DEFINE_PROP_UINT16("num-queues", VHostBlk, conf.num_queues,
                        VHOST_BLK_AUTO_NUM_QUEUES),
     DEFINE_PROP_UINT16("queue-size", VHostBlk, conf.queue_size, 256),
@@ -478,6 +501,7 @@ static void vhost_blk_instance_init(Object *obj)
     VHostBlk *s = VHOST_BLK(obj);
 
     s->vhostfd = -1;
+    s->backend_fd = -1;
     device_add_bootindex_property(obj, &s->conf.conf.bootindex,
                                   "bootindex", "/disk@0,0",
                                   DEVICE(obj));
diff --git a/include/hw/virtio/vhost-blk.h b/include/hw/virtio/vhost-blk.h
index 0c7e212595..c194b421d9 100644
--- a/include/hw/virtio/vhost-blk.h
+++ b/include/hw/virtio/vhost-blk.h
@@ -14,7 +14,6 @@
 #include "standard-headers/linux/virtio_blk.h"
 #include "hw/block/block.h"
 #include "hw/virtio/vhost.h"
-#include "system/block-backend.h"
 
 #define TYPE_VHOST_BLK "vhost-blk"
 #define VHOST_BLK(obj) \
@@ -25,6 +24,7 @@
 
 typedef struct VhostBlkConf {
     BlockConf conf;
+    char *devpath;
     uint16_t num_queues;
     uint16_t queue_size;
     uint16_t num_threads;
@@ -37,10 +37,11 @@ typedef struct VHostBlk {
     VMChangeStateEntry *mighand;
     uint64_t host_features;
     uint64_t decided_features;
-    struct virtio_blk_config blkcfg;
     int vhostfd;
+    int backend_fd;
     struct vhost_dev dev;
     bool vhost_started;
+    uint64_t length;
 } VHostBlk;
 
 #endif
-- 
2.43.5


^ permalink raw reply	[relevance 23%]

* [QEMU HCI-8.0 PATCH v3 5/5] vhost-blk: preserve the uevent socket across cpr-exec
                     ` (3 preceding siblings ...)
  2026-09-08  1:04 19% ` [QEMU HCI-8.0 PATCH v3 4/5] vhost-blk: watch the device for resize events Andrey Zhadchenko
@ 2026-09-08  1:04 21% ` Andrey Zhadchenko
  2026-09-08 11:11  7%   ` Andrey Drobyshev
  4 siblings, 1 reply; 200+ results
From: Andrey Zhadchenko @ 2026-09-08  1:04 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

qemu-update uses cpr-exec migration: QEMU re-execs itself in place.
A plain monitor fdset descriptor like our uevent socket does not
survive that (unlike the cpr_save_fd()'d tap/vhost fds), so without
help the resized-device notifications would silently stop until the
next full VM start.

Preserve it the same way the other devices do: on cold boot
cpr_save_fd() the socket under the "<proxy id>_ueventfd" key, and
on the re-exec'd (incoming) side adopt it with cpr_find_fd()
instead of reopening a command line fd that no longer resolves.
The management layer passes a placeholder for the "ueventfd"
property in that case.  The restored socket is validated just like
a cold-boot one.

Since a unique device ID is mandatory for FD preservation, block
CPR migration modes when the proxy has no ID, as other devices do.

Drop the saved descriptor on teardown so it does not outlive the
device.

https://virtuozzo.atlassian.net/browse/VSTOR-143437
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
v2:
 - old one dropped, setup moved to libvirt
 - added new patch to support uevent socket cpr

v3:
 - use the proxy ID for the CPR fd name and block CPR without an ID
 - validate restored sockets and improve failure cleanup

 hw/block/vhost-blk.c          | 77 +++++++++++++++++++++++++++++++----
 include/hw/virtio/vhost-blk.h |  1 +
 2 files changed, 71 insertions(+), 7 deletions(-)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index a69bdcb551..fa951da47a 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -29,6 +29,8 @@
 #include <sys/ioctl.h>
 #include <linux/fs.h>
 #include <linux/netlink.h>
+#include "migration/blocker.h"
+#include "migration/cpr.h"
 #include "system/runstate.h"
 
 static int vhost_blk_start(VirtIODevice *vdev)
@@ -248,12 +250,27 @@ static void vhost_blk_resize_bh(void *opaque)
     }
 }
 
+static char *vhost_blk_cpr_name(VHostBlk *s)
+{
+    DeviceState *proxy = qdev_get_parent_bus(DEVICE(s))->parent;
+
+    return proxy->id ? g_strdup_printf("%s_ueventfd", proxy->id) : NULL;
+}
+
 static void vhost_blk_uevent_detach(VHostBlk *s)
 {
+    g_autofree char *cpr_name = NULL;
+
+    migrate_del_blocker(&s->migration_blocker);
+
     if (s->uevent_fd < 0) {
         return;
     }
 
+    cpr_name = vhost_blk_cpr_name(s);
+    if (cpr_name) {
+        cpr_delete_fd(cpr_name, 0);
+    }
     qemu_set_fd_handler(s->uevent_fd, NULL, NULL, NULL);
     qemu_bh_delete(s->resize_bh);
     s->resize_bh = NULL;
@@ -395,25 +412,71 @@ static bool vhost_blk_uevent_check(int fd, const char *src, Error **errp)
 
 static bool vhost_blk_uevent_attach(VHostBlk *s, Error **errp)
 {
+    g_autofree char *cpr_name = NULL;
+
     if (!s->conf.ueventfd) {
         return true;
     }
 
-    s->uevent_fd = monitor_fd_param(monitor_cur(), s->conf.ueventfd, errp);
-    if (s->uevent_fd < 0) {
-        error_prepend(errp, "vhost-blk: unable to obtain uevent socket: ");
-        return false;
+    cpr_name = vhost_blk_cpr_name(s);
+
+    /*
+     * A unique ID is mandatory for preserving the uevent socket across
+     * CPR migration, so block CPR modes when it is absent.
+     */
+    if (!cpr_name) {
+        error_setg(&s->migration_blocker,
+                   "vhost-blk: device ID is required for CPR migration");
+        if (migrate_add_blocker_modes(&s->migration_blocker, errp,
+                                      MIG_MODE_CPR_TRANSFER,
+                                      MIG_MODE_CPR_EXEC, -1) < 0) {
+            return false;
+        }
+    }
+
+    if (cpr_is_incoming()) {
+        if (!cpr_name) {
+            error_setg(errp,
+                       "vhost-blk: device ID is required for CPR migration");
+            goto fail;
+        }
+        s->uevent_fd = cpr_find_fd(cpr_name, 0);
+        if (s->uevent_fd < 0) {
+            error_setg(errp,
+                       "vhost-blk: no preserved uevent socket to restore");
+            goto fail;
+        }
+    } else {
+        s->uevent_fd = monitor_fd_param(monitor_cur(), s->conf.ueventfd, errp);
+        if (s->uevent_fd < 0) {
+            error_prepend(errp, "vhost-blk: unable to obtain uevent socket: ");
+            goto fail;
+        }
     }
 
     if (!vhost_blk_uevent_check(s->uevent_fd, s->conf.ueventfd, errp)) {
-        qemu_close(s->uevent_fd);
-        s->uevent_fd = -1;
-        return false;
+        goto fail;
+    }
+
+    if (cpr_name && !cpr_is_incoming()) {
+        /* Preserve the socket across a future cpr-exec qemu-update. */
+        cpr_save_fd(cpr_name, 0, s->uevent_fd);
     }
 
     s->resize_bh = qemu_bh_new(vhost_blk_resize_bh, s);
     qemu_set_fd_handler(s->uevent_fd, vhost_blk_uevent_read, NULL, s);
     return true;
+
+fail:
+    if (s->uevent_fd >= 0) {
+        if (cpr_name && cpr_is_incoming()) {
+            cpr_delete_fd(cpr_name, 0);
+        }
+        qemu_close(s->uevent_fd);
+        s->uevent_fd = -1;
+    }
+    migrate_del_blocker(&s->migration_blocker);
+    return false;
 }
 
 static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
diff --git a/include/hw/virtio/vhost-blk.h b/include/hw/virtio/vhost-blk.h
index e3ad92bc6e..2d3f7a30f0 100644
--- a/include/hw/virtio/vhost-blk.h
+++ b/include/hw/virtio/vhost-blk.h
@@ -47,6 +47,7 @@ typedef struct VHostBlk {
     uint64_t length;
     uint64_t backend_rdev;
     QEMUBH *resize_bh;
+    Error *migration_blocker;   /* set when the device has no ID */
 } VHostBlk;
 
 #endif
-- 
2.43.5


^ permalink raw reply	[relevance 21%]

* Re: [Devel] [PATCH VZ10 3/3] ve_namespace: Allow entering non-running VE namespace
  2026-09-07 19:14  4% ` [Devel] [PATCH VZ10 3/3] ve_namespace: Allow entering non-running VE namespace Vladimir Riabchun
@ 2026-09-08  9:12  0%   ` Pavel Tikhomirov
  0 siblings, 0 replies; 200+ results
From: Pavel Tikhomirov @ 2026-09-08  9:12 UTC (permalink / raw)
  To: Konstantin Khorenko; +Cc: devel

> This doesn't bring a lot of value and makes tests more complex.
> To enter VE namespace we need to actually start VE.
> 
> 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.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-144504
> 
> Feature: ve selftests
> Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
>
> diff --git a/kernel/ve/ve_namespace.c b/kernel/ve/ve_namespace.c
> index 3dbe0dde2645d..2ddec9ef312f9 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("Switching to a non-running ve %s.\n"
> +			"This should happen in kselftests only.\n",

nit: I would rather avoid mentioning kselftests in a general warning the
customers might see.

-- 
Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* Re: [QEMU HCI-8.0 PATCH v3 2/5] vhost-blk: change backend setup
  2026-09-08  1:04 23% ` [QEMU HCI-8.0 PATCH v3 2/5] vhost-blk: change backend setup Andrey Zhadchenko
@ 2026-09-08 11:11  0%   ` Andrey Drobyshev
  0 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-08 11:11 UTC (permalink / raw)
  To: Andrey Zhadchenko; +Cc: svt-core, den, andrey.drobyshev

> Previously we used very ugly and incapsulation-breaking assignment
> fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
> It is wrong in a many ways, so let's rework this.
> 
> Patch changes default `drive` to new `devpath` option so device fd
> is managed by vhost-blk itself. Unfortunately this way we need a
> bit more preparational work: finding out disk length, etc. Don't
> be too broad and just do the minimal work.
> Also drop generic block device properties along with the block
> node: the kernel module does all IO in terms of 512 sectors, so
> simply report 512 byte logical/physical block size to the guest.
> Existing vhost-blk setups will break, but it is expected.
> 
> We also lose resize, as this is tied to the block node, which is
> now have no place in the setup. We will add this in the next
> patches as well as RO mode.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-143437
> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
>
> diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
> index 20c2f416916..d6681235e5d 100644
> --- a/hw/block/vhost-blk.c
> +++ b/hw/block/vhost-blk.c
> @@ -25,22 +25,17 @@
>  #include "linux-headers/linux/vhost.h"
>  #include <sys/ioctl.h>
>  #include <linux/fs.h>
> -#include "include/block/block_int-common.h"
>  #include "system/runstate.h"
>  
>  static int vhost_blk_start(VirtIODevice *vdev)
>  {
>      VHostBlk *s = VHOST_BLK(vdev);
>      struct vhost_vring_file backend;
> -    int ret, i, nworkers, *fd;
> +    int ret, i, nworkers;
>      BusState *qbus = BUS(qdev_get_parent_bus(DEVICE(vdev)));
>      VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
>      char serial[VIRTIO_BLK_ID_BYTES] = {0};
>  
> -    bdrv_graph_rdlock_main_loop();
> -    fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
> -    bdrv_graph_rdunlock_main_loop();
> -
>      if (!k->set_guest_notifiers) {
>          error_report("vhost-blk: binding does not support guest notifiers");
>          return -ENOSYS;
> @@ -92,7 +87,7 @@ static int vhost_blk_start(VirtIODevice *vdev)
>  
>      memset(&backend, 0, sizeof(backend));
>      backend.index = 0;
> -    backend.fd = *fd;
> +    backend.fd = s->backend_fd;
>      if (ioctl(s->vhostfd, VHOST_BLK_SET_BACKEND, &backend)) {
>          error_report("vhost-blk: unable to set backend");
>          ret = -errno;
> @@ -208,29 +203,69 @@ static void vhost_blk_vm_state(void *opaque, bool running, RunState state)
>      }
>  }
>  
> -static void vhost_blk_resize_cb(void *opaque)
> +static bool vhost_blk_update_size(VHostBlk *s, Error **errp)
>  {
> -    VirtIODevice *vdev = opaque;
> +    BlockConf *conf = &s->conf.conf;
> +    uint64_t length;
> +    bool changed;
> +
> +    if (ioctl(s->backend_fd, BLKGETSIZE64, &length) < 0) {
> +        error_setg_errno(errp, errno,
> +                         "vhost-blk: unable to determine size of '%s'",
> +                         s->conf.devpath);
> +        return false;
> +    }
>  
> -    assert(qemu_get_current_aio_context() == qemu_get_aio_context());
> -    virtio_notify_config(vdev);
> +    changed = s->length != length;
> +    s->length = length;
> +    conf->heads = 16;
> +    conf->secs = 63;
> +    conf->cyls = s->length / BDRV_SECTOR_SIZE /
> +                 (conf->heads * conf->secs);
> +    conf->cyls = MIN(MAX(conf->cyls, 2U), 16383U);

This hunk haven't changed since v2, but I've only noticed now.  Where do
these constants come from?  This code is extremely obscure as-is, no
explanation, no comment.  Are we applying the same logic here as in
hw/block/hd-geometry.c ? If so, let's just factor out some helper from
there and reuse it.  If it's inconvenient - at least comment where do
these values come from.

Another thing: these geometry values don't change over time, why we set
them on every call to update_size()?  Maybe just put them into
open_backend(), together with setting logical/physical block sizes.

Andrey

-- 
Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

^ permalink raw reply	[relevance 0%]

* Re: [QEMU HCI-8.0 PATCH v3 5/5] vhost-blk: preserve the uevent socket across cpr-exec
  2026-09-08  1:04 21% ` [QEMU HCI-8.0 PATCH v3 5/5] vhost-blk: preserve the uevent socket across cpr-exec Andrey Zhadchenko
@ 2026-09-08 11:11  7%   ` Andrey Drobyshev
  0 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-08 11:11 UTC (permalink / raw)
  To: Andrey Zhadchenko; +Cc: svt-core, den, andrey.drobyshev

> qemu-update uses cpr-exec migration: QEMU re-execs itself in place.
> A plain monitor fdset descriptor like our uevent socket does not
> survive that (unlike the cpr_save_fd()'d tap/vhost fds), so without
> help the resized-device notifications would silently stop until the
> next full VM start.
> 
> Preserve it the same way the other devices do: on cold boot
> cpr_save_fd() the socket under the "<proxy id>_ueventfd" key, and
> on the re-exec'd (incoming) side adopt it with cpr_find_fd()
> instead of reopening a command line fd that no longer resolves.
> The management layer passes a placeholder for the "ueventfd"
> property in that case.  The restored socket is validated just like
> a cold-boot one.
> 
> Since a unique device ID is mandatory for FD preservation, block
> CPR migration modes when the proxy has no ID, as other devices do.
> 
> Drop the saved descriptor on teardown so it does not outlive the
> device.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-143437
> Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
>
> diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
> index a69bdcb551f..fa951da47a6 100644
> --- a/hw/block/vhost-blk.c
> +++ b/hw/block/vhost-blk.c
> @@ -29,6 +29,8 @@
>  #include <sys/ioctl.h>
>  #include <linux/fs.h>
>  #include <linux/netlink.h>
> +#include "migration/blocker.h"
> +#include "migration/cpr.h"
>  #include "system/runstate.h"
>  
>  static int vhost_blk_start(VirtIODevice *vdev)
> @@ -248,12 +250,27 @@ static void vhost_blk_resize_bh(void *opaque)
>      }
>  }
>  
> +static char *vhost_blk_cpr_name(VHostBlk *s)
> +{
> +    DeviceState *proxy = qdev_get_parent_bus(DEVICE(s))->parent;
> +
> +    return proxy->id ? g_strdup_printf("%s_ueventfd", proxy->id) : NULL;
> +}
> +
>  static void vhost_blk_uevent_detach(VHostBlk *s)
>  {
> +    g_autofree char *cpr_name = NULL;
> +
> +    migrate_del_blocker(&s->migration_blocker);
> +
>      if (s->uevent_fd < 0) {
>          return;
>      }
>  
> +    cpr_name = vhost_blk_cpr_name(s);
> +    if (cpr_name) {
> +        cpr_delete_fd(cpr_name, 0);
> +    }
>      qemu_set_fd_handler(s->uevent_fd, NULL, NULL, NULL);
>      qemu_bh_delete(s->resize_bh);
>      s->resize_bh = NULL;
> @@ -395,25 +412,71 @@ static bool vhost_blk_uevent_check(int fd, const char *src, Error **errp)
>  
>  static bool vhost_blk_uevent_attach(VHostBlk *s, Error **errp)
>  {
> +    g_autofree char *cpr_name = NULL;
> +
>      if (!s->conf.ueventfd) {
>          return true;
>      }

On CPR target, if libvirt (mistakenly) skips ueventfd param in cmdline,
we return 'true' here, socket remains open, and there's no resize
detection.  I'd prefer spitting an error loud and clear in such a case.

>  
> -    s->uevent_fd = monitor_fd_param(monitor_cur(), s->conf.ueventfd, errp);
> -    if (s->uevent_fd < 0) {
> -        error_prepend(errp, "vhost-blk: unable to obtain uevent socket: ");
> -        return false;
> +    cpr_name = vhost_blk_cpr_name(s);
> +
> +    /*
> +     * A unique ID is mandatory for preserving the uevent socket across
> +     * CPR migration, so block CPR modes when it is absent.
> +     */
> +    if (!cpr_name) {
> +        error_setg(&s->migration_blocker,
> +                   "vhost-blk: device ID is required for CPR migration");
> +        if (migrate_add_blocker_modes(&s->migration_blocker, errp,
> +                                      MIG_MODE_CPR_TRANSFER,
> +                                      MIG_MODE_CPR_EXEC, -1) < 0) {
> +            return false;
> +        }
> +    }
> +
> +    if (cpr_is_incoming()) {
> +        if (!cpr_name) {
> +            error_setg(errp,
> +                       "vhost-blk: device ID is required for CPR migration");
> +            goto fail;
> +        }
> +        s->uevent_fd = cpr_find_fd(cpr_name, 0);
> +        if (s->uevent_fd < 0) {
> +            error_setg(errp,
> +                       "vhost-blk: no preserved uevent socket to restore");
> +            goto fail;
> +        }
> +    } else {
> +        s->uevent_fd = monitor_fd_param(monitor_cur(), s->conf.ueventfd, errp);
> +        if (s->uevent_fd < 0) {
> +            error_prepend(errp, "vhost-blk: unable to obtain uevent socket: ");
> +            goto fail;
> +        }
>      }
>  
>      if (!vhost_blk_uevent_check(s->uevent_fd, s->conf.ueventfd, errp)) {

Nit: on CPR target, s->conf.ueventfd is likely to be a placeholder string.

> -        qemu_close(s->uevent_fd);
> -        s->uevent_fd = -1;
> -        return false;
> +        goto fail;
> +    }
> +
> +    if (cpr_name && !cpr_is_incoming()) {
> +        /* Preserve the socket across a future cpr-exec qemu-update. */
> +        cpr_save_fd(cpr_name, 0, s->uevent_fd);
>      }
>  
>      s->resize_bh = qemu_bh_new(vhost_blk_resize_bh, s);
>      qemu_set_fd_handler(s->uevent_fd, vhost_blk_uevent_read, NULL, s);
>      return true;
> +
> +fail:
> +    if (s->uevent_fd >= 0) {
> +        if (cpr_name && cpr_is_incoming()) {
> +            cpr_delete_fd(cpr_name, 0);
> +        }
> +        qemu_close(s->uevent_fd);
> +        s->uevent_fd = -1;
> +    }
> +    migrate_del_blocker(&s->migration_blocker);

On cpr_is_incoming() && !cpr_name, we're gonna add blocker, goto fail,
then delete blocker.  Redundant.

> +    return false;
>  }

Consider the following overall shape (designed by Claude, so please
recheck):

  static bool vhost_blk_uevent_attach(VHostBlk *s, Error **errp)
  {
      g_autofree char *cpr_name = vhost_blk_cpr_name(s);
      bool incoming = cpr_is_incoming();
      int saved_fd = -1;

      if (incoming && cpr_name) {
          saved_fd = cpr_find_fd(cpr_name, 0);
      }

      if (!s->conf.ueventfd) {
          if (saved_fd >= 0) {
              error_setg(errp, "vhost-blk: a preserved uevent socket exists "
                         "but the ueventfd property is not set");
              return false;
          }
          return true;
      }

      if (incoming) {
          if (saved_fd < 0) {
              error_setg(errp, "vhost-blk: no preserved uevent socket to "
                         "restore%s", cpr_name ? "" : " (device has no ID)");
              return false;
          }
          s->uevent_fd = saved_fd;
      } else {
          s->uevent_fd = monitor_fd_param(monitor_cur(), s->conf.ueventfd, errp);
          if (s->uevent_fd < 0) {
              error_prepend(errp, "vhost-blk: unable to obtain uevent socket: ");
              return false;
          }
      }

      if (!vhost_blk_uevent_check(s->uevent_fd,
                                  incoming ? cpr_name : s->conf.ueventfd, errp)) {
          goto fail;
      }

      if (!incoming) {
          if (cpr_name) {
              /* Preserve the socket across a future CPR migration. */
              cpr_save_fd(cpr_name, 0, s->uevent_fd);
          } else {
              error_setg(&s->migration_blocker,
                         "vhost-blk: device ID is required for CPR migration");
              if (migrate_add_blocker_modes(&s->migration_blocker, errp,
                                            MIG_MODE_CPR_TRANSFER,
                                            MIG_MODE_CPR_EXEC, -1) < 0) {
                  goto fail;
              }
          }
      }

      s->resize_bh = qemu_bh_new(vhost_blk_resize_bh, s);
      qemu_set_fd_handler(s->uevent_fd, vhost_blk_uevent_read, NULL, s);
      return true;

  fail:
      if (incoming) {
          cpr_delete_fd(cpr_name, 0);
      }
      qemu_close(s->uevent_fd);
      s->uevent_fd = -1;
      return false;
  }

Andrey

-- 
Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

^ permalink raw reply	[relevance 7%]

* [QEMU HCI-8.0 PATCH v4 3/5] vhost-blk: add read-only flag
    2026-09-08 12:52 21% ` [QEMU HCI-8.0 PATCH v4 1/5] vhost-blk: improve vhost_blk_device_realize cleanup Andrey Zhadchenko
  2026-09-08 12:52 23% ` [QEMU HCI-8.0 PATCH v4 2/5] vhost-blk: change backend setup Andrey Zhadchenko
@ 2026-09-08 12:52 27% ` Andrey Zhadchenko
  2026-09-08 12:52 19% ` [QEMU HCI-8.0 PATCH v4 4/5] vhost-blk: watch the device for resize events Andrey Zhadchenko
  2026-09-08 12:52 24% ` [QEMU HCI-8.0 PATCH v4 5/5] vhost-blk: preserve the uevent socket across cpr-exec Andrey Zhadchenko
  4 siblings, 0 replies; 200+ results
From: Andrey Zhadchenko @ 2026-09-08 12:52 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

Also compare BLKROGET with the selected mode and reject r/w if
needed.

https://virtuozzo.atlassian.net/browse/VSTOR-143437
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
v2:
 - check BLKROGET only if rw was requested like posix_file

 hw/block/vhost-blk.c          | 25 ++++++++++++++++++++++++-
 include/hw/virtio/vhost-blk.h |  1 +
 2 files changed, 25 insertions(+), 1 deletion(-)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index a517c5e22b..dc21f9fbbf 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -234,8 +234,9 @@ static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
     ERRP_GUARD();
     BlockConf *conf = &s->conf.conf;
     struct stat st;
+    int open_flags = s->conf.readonly ? O_RDONLY : O_RDWR;
 
-    s->backend_fd = qemu_open(s->conf.devpath, O_RDWR, errp);
+    s->backend_fd = qemu_open(s->conf.devpath, open_flags, errp);
     if (s->backend_fd < 0) {
         error_prepend(errp, "vhost-blk: unable to open backend: ");
         return false;
@@ -253,6 +254,23 @@ static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
         goto fail;
     }
 
+    if (!s->conf.readonly) {
+        int readonly;
+
+        if (ioctl(s->backend_fd, BLKROGET, &readonly) < 0) {
+            error_setg_errno(errp, errno,
+                             "vhost-blk: unable to get read-only status of "
+                             "'%s'", s->conf.devpath);
+            goto fail;
+        }
+
+        if (readonly) {
+            error_setg_errno(errp, EROFS, "vhost-blk: '%s' is not writable",
+                             s->conf.devpath);
+            goto fail;
+        }
+    }
+
     vhost_blk_update_size(s, errp);
     if (*errp) {
         goto fail;
@@ -411,6 +429,10 @@ static uint64_t vhost_blk_get_features(VirtIODevice *vdev,
 
     virtio_add_feature(&features, VIRTIO_F_VERSION_1);
 
+    if (s->conf.readonly) {
+        virtio_add_feature(&features, VIRTIO_BLK_F_RO);
+    }
+
     if (s->conf.num_queues > 1) {
         virtio_add_feature(&features, VIRTIO_BLK_F_MQ);
     }
@@ -458,6 +480,7 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
 
 static const Property vhost_blk_properties[] = {
     DEFINE_PROP_STRING("devpath", VHostBlk, conf.devpath),
+    DEFINE_PROP_BOOL("read-only", VHostBlk, conf.readonly, false),
     DEFINE_PROP_UINT16("num-queues", VHostBlk, conf.num_queues,
                        VHOST_BLK_AUTO_NUM_QUEUES),
     DEFINE_PROP_UINT16("queue-size", VHostBlk, conf.queue_size, 256),
diff --git a/include/hw/virtio/vhost-blk.h b/include/hw/virtio/vhost-blk.h
index c194b421d9..c6646f5845 100644
--- a/include/hw/virtio/vhost-blk.h
+++ b/include/hw/virtio/vhost-blk.h
@@ -25,6 +25,7 @@
 typedef struct VhostBlkConf {
     BlockConf conf;
     char *devpath;
+    bool readonly;
     uint16_t num_queues;
     uint16_t queue_size;
     uint16_t num_threads;
-- 
2.43.5


^ permalink raw reply	[relevance 27%]

* [QEMU HCI-8.0 PATCH v4 4/5] vhost-blk: watch the device for resize events
                     ` (2 preceding siblings ...)
  2026-09-08 12:52 27% ` [QEMU HCI-8.0 PATCH v4 3/5] vhost-blk: add read-only flag Andrey Zhadchenko
@ 2026-09-08 12:52 19% ` Andrey Zhadchenko
  2026-09-08 12:52 24% ` [QEMU HCI-8.0 PATCH v4 5/5] vhost-blk: preserve the uevent socket across cpr-exec Andrey Zhadchenko
  4 siblings, 0 replies; 200+ results
From: Andrey Zhadchenko @ 2026-09-08 12:52 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

Resize was tied to block node, which we removed a few patches ago.
Luckily we can make resize automated: receive an uevent socket
from the management layer via the new "ueventfd" property (a
monitor fd name passed with the 'getfd' QMP command), watch it for
relevant netlink messages and call virtio_notify_config() if we
detect a capacity change.

The socket is set up (possibly with filter) by management layer.
We only need to check that it is netlink, subscribed to kernel
uevents, and do some message filtering.

When the property is not set, capacity changes are not detected.

https://virtuozzo.atlassian.net/browse/VSTOR-143437
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
v2:
 - rework patch a bit: now we expect opened and filtered fd from
libvirt.

v3:
 - accept a monitor fd name passed with getfd instead of a
/dev/fdset path
 - verify that the socket is subscribed to kernel uevents
 - detach the socket after a permanent receive error
 - change buffer capacity to 4KiB

v4:
 - keep the uevent socket local until it has been validated

 hw/block/vhost-blk.c          | 202 ++++++++++++++++++++++++++++++++++
 include/hw/virtio/vhost-blk.h |   4 +
 2 files changed, 206 insertions(+)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index dc21f9fbbf..ef46c55665 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -10,7 +10,10 @@
 
 #include "qemu/osdep.h"
 #include "qapi/error.h"
+#include "qemu/cutils.h"
 #include "qemu/error-report.h"
+#include "qemu/main-loop.h"
+#include "monitor/monitor.h"
 #include "qom/object.h"
 #include "hw/qdev-core.h"
 #include "hw/boards.h"
@@ -25,6 +28,7 @@
 #include "linux-headers/linux/vhost.h"
 #include <sys/ioctl.h>
 #include <linux/fs.h>
+#include <linux/netlink.h>
 #include "system/runstate.h"
 
 static int vhost_blk_start(VirtIODevice *vdev)
@@ -229,6 +233,193 @@ static bool vhost_blk_update_size(VHostBlk *s, Error **errp)
     return changed;
 }
 
+static void vhost_blk_resize_bh(void *opaque)
+{
+    VHostBlk *s = opaque;
+    Error *local_err = NULL;
+    bool changed;
+
+    changed = vhost_blk_update_size(s, &local_err);
+    if (local_err) {
+        error_report_err(local_err);
+        return;
+    }
+
+    if (changed) {
+        virtio_notify_config(VIRTIO_DEVICE(s));
+    }
+}
+
+static void vhost_blk_uevent_detach(VHostBlk *s)
+{
+    if (s->uevent_fd < 0) {
+        return;
+    }
+
+    qemu_set_fd_handler(s->uevent_fd, NULL, NULL, NULL);
+    qemu_bh_delete(s->resize_bh);
+    s->resize_bh = NULL;
+    qemu_close(s->uevent_fd);
+    s->uevent_fd = -1;
+}
+
+/*
+ * The uevent socket is created, bound and filtered by the management
+ * layer and passed to us via the "ueventfd" property.
+ */
+static void vhost_blk_uevent_read(void *opaque)
+{
+    VHostBlk *s = opaque;
+    char buffer[4096];
+
+    for (;;) {
+        struct sockaddr_nl source;
+        socklen_t source_len = sizeof(source);
+        uint64_t event_major = UINT64_MAX;
+        uint64_t event_minor = UINT64_MAX;
+        bool action_change = false;
+        bool subsystem_block = false;
+        bool resize = false;
+        char *field;
+        char *end;
+        ssize_t len;
+
+        memset(&source, 0, sizeof(source));
+        len = recvfrom(s->uevent_fd, buffer, sizeof(buffer) - 1,
+                       MSG_DONTWAIT, (struct sockaddr *)&source, &source_len);
+        if (len < 0) {
+            if (errno == EINTR) {
+                continue;
+            }
+            if (errno == ENOBUFS) {
+                /* Some events may be dropped, just re-check */
+                qemu_bh_schedule(s->resize_bh);
+                continue;
+            }
+            if (errno != EAGAIN && errno != EWOULDBLOCK) {
+                error_report("vhost-blk: detaching uevent after the error: %s",
+                             strerror(errno));
+                vhost_blk_uevent_detach(s);
+            }
+            return;
+        }
+
+        if (source.nl_family != AF_NETLINK || source.nl_pid != 0) {
+            continue;
+        }
+
+        buffer[len] = '\0';
+        field = buffer;
+        end = buffer + len;
+        while (field < end) {
+            size_t field_len = strnlen(field, end - field);
+
+            if (!strcmp(field, "ACTION=change")) {
+                action_change = true;
+            } else if (!strcmp(field, "SUBSYSTEM=block")) {
+                subsystem_block = true;
+            } else if (!strcmp(field, "RESIZE=1")) {
+                resize = true;
+            } else if (g_str_has_prefix(field, "MAJOR=")) {
+                uint64_t value;
+
+                if (!qemu_strtou64(field + strlen("MAJOR="), NULL, 10,
+                                   &value)) {
+                    event_major = value;
+                }
+            } else if (g_str_has_prefix(field, "MINOR=")) {
+                uint64_t value;
+
+                if (!qemu_strtou64(field + strlen("MINOR="), NULL, 10,
+                                   &value)) {
+                    event_minor = value;
+                }
+            }
+
+            if (field_len == end - field) {
+                break;
+            }
+            field += field_len + 1;
+        }
+
+        if (action_change && subsystem_block && resize &&
+            event_major == major(s->backend_rdev) &&
+            event_minor == minor(s->backend_rdev)) {
+            qemu_bh_schedule(s->resize_bh);
+        }
+    }
+}
+
+static bool vhost_blk_uevent_check(int fd, const char *src, Error **errp)
+{
+    struct sockaddr_nl addr;
+    socklen_t addrlen;
+    socklen_t optlen;
+    int domain;
+    int protocol;
+
+    optlen = sizeof(domain);
+    if (getsockopt(fd, SOL_SOCKET, SO_DOMAIN, &domain, &optlen) < 0) {
+        error_setg_errno(errp, errno, "vhost-blk: '%s' is not a socket", src);
+        return false;
+    }
+
+    optlen = sizeof(protocol);
+    if (getsockopt(fd, SOL_SOCKET, SO_PROTOCOL, &protocol, &optlen) < 0) {
+        error_setg_errno(errp, errno,
+                         "vhost-blk: unable to get protocol of '%s'", src);
+        return false;
+    }
+
+    if (domain != AF_NETLINK || protocol != NETLINK_KOBJECT_UEVENT) {
+        error_setg(errp,
+                   "vhost-blk: '%s' is not a NETLINK_KOBJECT_UEVENT socket",
+                   src);
+        return false;
+    }
+
+    addrlen = sizeof(addr);
+    if (getsockname(fd, (struct sockaddr *)&addr, &addrlen) < 0) {
+        error_setg_errno(errp, errno,
+                         "vhost-blk: unable to get address of '%s'", src);
+        return false;
+    }
+
+    if (addr.nl_groups != 1) {
+        error_setg(errp,
+                   "vhost-blk: '%s' is not subscribed to kernel uevents",
+                   src);
+        return false;
+    }
+
+    return true;
+}
+
+static bool vhost_blk_uevent_attach(VHostBlk *s, Error **errp)
+{
+    int fd;
+
+    if (!s->conf.ueventfd) {
+        return true;
+    }
+
+    fd = monitor_fd_param(monitor_cur(), s->conf.ueventfd, errp);
+    if (fd < 0) {
+        error_prepend(errp, "vhost-blk: unable to obtain uevent socket: ");
+        return false;
+    }
+
+    if (!vhost_blk_uevent_check(fd, s->conf.ueventfd, errp)) {
+        qemu_close(fd);
+        return false;
+    }
+
+    s->uevent_fd = fd;
+    s->resize_bh = qemu_bh_new(vhost_blk_resize_bh, s);
+    qemu_set_fd_handler(s->uevent_fd, vhost_blk_uevent_read, NULL, s);
+    return true;
+}
+
 static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
 {
     ERRP_GUARD();
@@ -253,6 +444,7 @@ static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
                    s->conf.devpath);
         goto fail;
     }
+    s->backend_rdev = st.st_rdev;
 
     if (!s->conf.readonly) {
         int readonly;
@@ -328,6 +520,12 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
         return;
     }
 
+    if (!vhost_blk_uevent_attach(s, errp)) {
+        qemu_close(s->backend_fd);
+        s->backend_fd = -1;
+        return;
+    }
+
     s->dev.nvqs = conf->num_queues;
     s->dev.max_queues = conf->num_queues;
     vqs = g_new0(struct vhost_virtqueue, s->dev.nvqs);
@@ -378,6 +576,7 @@ cleanup:
         qemu_close(s->backend_fd);
         s->backend_fd = -1;
     }
+    vhost_blk_uevent_detach(s);
     for (i = 0; i < conf->num_queues; i++) {
         virtio_del_queue(vdev, i);
     }
@@ -390,6 +589,7 @@ static void vhost_blk_device_unrealize(DeviceState *dev)
     VirtIODevice *vdev = VIRTIO_DEVICE(dev);
     VHostBlk *s = VHOST_BLK(dev);
 
+    vhost_blk_uevent_detach(s);
     qemu_del_vm_change_state_handler(s->mighand);
     vhost_blk_set_status(vdev, 0);
     vhost_dev_cleanup(&s->dev);
@@ -480,6 +680,7 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
 
 static const Property vhost_blk_properties[] = {
     DEFINE_PROP_STRING("devpath", VHostBlk, conf.devpath),
+    DEFINE_PROP_STRING("ueventfd", VHostBlk, conf.ueventfd),
     DEFINE_PROP_BOOL("read-only", VHostBlk, conf.readonly, false),
     DEFINE_PROP_UINT16("num-queues", VHostBlk, conf.num_queues,
                        VHOST_BLK_AUTO_NUM_QUEUES),
@@ -527,6 +728,7 @@ static void vhost_blk_instance_init(Object *obj)
 
     s->vhostfd = -1;
     s->backend_fd = -1;
+    s->uevent_fd = -1;
     device_add_bootindex_property(obj, &s->conf.conf.bootindex,
                                   "bootindex", "/disk@0,0",
                                   DEVICE(obj));
diff --git a/include/hw/virtio/vhost-blk.h b/include/hw/virtio/vhost-blk.h
index c6646f5845..e3ad92bc6e 100644
--- a/include/hw/virtio/vhost-blk.h
+++ b/include/hw/virtio/vhost-blk.h
@@ -25,6 +25,7 @@
 typedef struct VhostBlkConf {
     BlockConf conf;
     char *devpath;
+    char *ueventfd;
     bool readonly;
     uint16_t num_queues;
     uint16_t queue_size;
@@ -40,9 +41,12 @@ typedef struct VHostBlk {
     uint64_t decided_features;
     int vhostfd;
     int backend_fd;
+    int uevent_fd;
     struct vhost_dev dev;
     bool vhost_started;
     uint64_t length;
+    uint64_t backend_rdev;
+    QEMUBH *resize_bh;
 } VHostBlk;
 
 #endif
-- 
2.43.5


^ permalink raw reply	[relevance 19%]

* [QEMU HCI-8.0 PATCH v4 5/5] vhost-blk: preserve the uevent socket across cpr-exec
                     ` (3 preceding siblings ...)
  2026-09-08 12:52 19% ` [QEMU HCI-8.0 PATCH v4 4/5] vhost-blk: watch the device for resize events Andrey Zhadchenko
@ 2026-09-08 12:52 24% ` Andrey Zhadchenko
  4 siblings, 0 replies; 200+ results
From: Andrey Zhadchenko @ 2026-09-08 12:52 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

qemu-update uses cpr-exec migration: QEMU re-execs itself in place.
A plain monitor fdset descriptor like our uevent socket does not
survive that (unlike the cpr_save_fd()'d tap/vhost fds), so without
help the resized-device notifications would silently stop until the
next full VM start.

Preserve it the same way the other devices do: on cold boot
cpr_save_fd() the socket under the "<proxy id>_ueventfd" key, and
on the re-exec'd (incoming) side adopt it with cpr_find_fd()
instead of reopening a command line fd that no longer resolves.
The management layer passes a placeholder for the "ueventfd"
property in that case.  The restored socket is validated just like
a cold-boot one.

Block CPR migration modes when the proxy has no ID or we disabled
the socket after the error.

Drop the saved descriptor on teardown so it does not outlive the
device.

https://virtuozzo.atlassian.net/browse/VSTOR-143437
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
v2:
 - old one dropped, setup moved to libvirt
 - added new patch to support uevent socket cpr

v3:
 - use the proxy ID for the CPR fd name and block CPR without an ID
 - validate restored sockets and improve failure cleanup

v4:
 - split cold-boot and restore socket setup paths
 - reject restored socket/property mismatches
 - block CPR after a permanent uevent receive error

 hw/block/vhost-blk.c          | 107 +++++++++++++++++++++++++++++++++-
 include/hw/virtio/vhost-blk.h |   1 +
 2 files changed, 105 insertions(+), 3 deletions(-)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index ef46c55665..abddff02c6 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -29,6 +29,8 @@
 #include <sys/ioctl.h>
 #include <linux/fs.h>
 #include <linux/netlink.h>
+#include "migration/blocker.h"
+#include "migration/cpr.h"
 #include "system/runstate.h"
 
 static int vhost_blk_start(VirtIODevice *vdev)
@@ -250,12 +252,27 @@ static void vhost_blk_resize_bh(void *opaque)
     }
 }
 
+static char *vhost_blk_cpr_name(VHostBlk *s)
+{
+    DeviceState *proxy = qdev_get_parent_bus(DEVICE(s))->parent;
+
+    return proxy->id ? g_strdup_printf("%s_ueventfd", proxy->id) : NULL;
+}
+
 static void vhost_blk_uevent_detach(VHostBlk *s)
 {
+    g_autofree char *cpr_name = NULL;
+
+    migrate_del_blocker(&s->cpr_blocker);
+
     if (s->uevent_fd < 0) {
         return;
     }
 
+    cpr_name = vhost_blk_cpr_name(s);
+    if (cpr_name) {
+        cpr_delete_fd(cpr_name, 0);
+    }
     qemu_set_fd_handler(s->uevent_fd, NULL, NULL, NULL);
     qemu_bh_delete(s->resize_bh);
     s->resize_bh = NULL;
@@ -263,6 +280,19 @@ static void vhost_blk_uevent_detach(VHostBlk *s)
     s->uevent_fd = -1;
 }
 
+static void vhost_blk_block_cpr(VHostBlk *s)
+{
+    Error *local_err = NULL;
+
+    error_setg(&s->cpr_blocker,
+               "vhost-blk: the uevent socket was detached after an error");
+    if (migrate_add_blocker_modes(&s->cpr_blocker, &local_err,
+                                  MIG_MODE_CPR_TRANSFER,
+                                  MIG_MODE_CPR_EXEC, -1) < 0) {
+        error_report_err(local_err);
+    }
+}
+
 /*
  * The uevent socket is created, bound and filtered by the management
  * layer and passed to us via the "ueventfd" property.
@@ -300,6 +330,7 @@ static void vhost_blk_uevent_read(void *opaque)
                 error_report("vhost-blk: detaching uevent after the error: %s",
                              strerror(errno));
                 vhost_blk_uevent_detach(s);
+                vhost_blk_block_cpr(s);
             }
             return;
         }
@@ -395,7 +426,41 @@ static bool vhost_blk_uevent_check(int fd, const char *src, Error **errp)
     return true;
 }
 
-static bool vhost_blk_uevent_attach(VHostBlk *s, Error **errp)
+static bool vhost_blk_uevent_restore(VHostBlk *s, const char *cpr_name,
+                                     Error **errp)
+{
+    int fd = -1;
+
+    if (cpr_name) {
+        fd = cpr_find_fd(cpr_name, 0);
+    }
+
+    if (!s->conf.ueventfd) {
+        if (fd >= 0) {
+            error_setg(errp, "vhost-blk: a preserved uevent socket exists "
+                       "but the 'ueventfd' property is not configured");
+            return false;
+        }
+        return true;
+    }
+
+    if (fd < 0) {
+        error_setg(errp, "vhost-blk: no preserved uevent socket to restore");
+        return false;
+    }
+
+    if (!vhost_blk_uevent_check(fd, cpr_name, errp)) {
+        cpr_delete_fd(cpr_name, 0);
+        qemu_close(fd);
+        return false;
+    }
+
+    s->uevent_fd = fd;
+    return true;
+}
+
+static bool vhost_blk_uevent_open(VHostBlk *s, const char *cpr_name,
+                                  Error **errp)
 {
     int fd;
 
@@ -403,18 +468,54 @@ static bool vhost_blk_uevent_attach(VHostBlk *s, Error **errp)
         return true;
     }
 
+    if (!cpr_name) {
+        error_setg(&s->cpr_blocker,
+                   "vhost-blk: device ID is required for CPR migration");
+        if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+                                      MIG_MODE_CPR_TRANSFER,
+                                      MIG_MODE_CPR_EXEC, -1) < 0) {
+            return false;
+        }
+    }
+
     fd = monitor_fd_param(monitor_cur(), s->conf.ueventfd, errp);
     if (fd < 0) {
         error_prepend(errp, "vhost-blk: unable to obtain uevent socket: ");
-        return false;
+        goto fail;
     }
 
     if (!vhost_blk_uevent_check(fd, s->conf.ueventfd, errp)) {
         qemu_close(fd);
-        return false;
+        goto fail;
+    }
+
+    if (cpr_name) {
+        cpr_save_fd(cpr_name, 0, fd);
     }
 
     s->uevent_fd = fd;
+    return true;
+
+fail:
+    migrate_del_blocker(&s->cpr_blocker);
+    return false;
+}
+
+static bool vhost_blk_uevent_attach(VHostBlk *s, Error **errp)
+{
+    g_autofree char *cpr_name = vhost_blk_cpr_name(s);
+    bool ok;
+
+    ok = cpr_is_incoming() ? vhost_blk_uevent_restore(s, cpr_name, errp)
+                           : vhost_blk_uevent_open(s, cpr_name, errp);
+    if (!ok) {
+        return false;
+    }
+
+    if (s->uevent_fd < 0) {
+        return true;
+    }
+
     s->resize_bh = qemu_bh_new(vhost_blk_resize_bh, s);
     qemu_set_fd_handler(s->uevent_fd, vhost_blk_uevent_read, NULL, s);
     return true;
diff --git a/include/hw/virtio/vhost-blk.h b/include/hw/virtio/vhost-blk.h
index e3ad92bc6e..059f1ebc70 100644
--- a/include/hw/virtio/vhost-blk.h
+++ b/include/hw/virtio/vhost-blk.h
@@ -47,6 +47,7 @@ typedef struct VHostBlk {
     uint64_t length;
     uint64_t backend_rdev;
     QEMUBH *resize_bh;
+    Error *cpr_blocker;
 } VHostBlk;
 
 #endif
-- 
2.43.5


^ permalink raw reply	[relevance 24%]

* [QEMU HCI-8.0 PATCH v4 2/5] vhost-blk: change backend setup
    2026-09-08 12:52 21% ` [QEMU HCI-8.0 PATCH v4 1/5] vhost-blk: improve vhost_blk_device_realize cleanup Andrey Zhadchenko
@ 2026-09-08 12:52 23% ` Andrey Zhadchenko
  2026-09-08 13:34  0%   ` Andrey Drobyshev
  2026-09-08 12:52 27% ` [QEMU HCI-8.0 PATCH v4 3/5] vhost-blk: add read-only flag Andrey Zhadchenko
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 200+ results
From: Andrey Zhadchenko @ 2026-09-08 12:52 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

Previously we used very ugly and incapsulation-breaking assignment
fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
It is wrong in a many ways, so let's rework this.

Patch changes default `drive` to new `devpath` option so device fd
is managed by vhost-blk itself. Unfortunately this way we need a
bit more preparational work: finding out disk length, etc. Don't
be too broad and just do the minimal work.
Also drop generic block device properties along with the block
node: the kernel module does all IO in terms of 512 sectors, so
simply report 512 byte logical/physical block size to the guest.
Existing vhost-blk setups will break, but it is expected.

We also lose resize, as this is tied to the block node, which is
now have no place in the setup. We will add this in the next
patches as well as RO mode.

https://virtuozzo.atlassian.net/browse/VSTOR-143437
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
v2:
 - moved double close fix to 'do not double close vhostfd' patch
 - dropped DEFINE_BLOCK_PROPERTIES_BASE exposition to the outside. Let's
just use defaults for now. We can later check if tweaking this values
makes any difference and add them back.
 - change vhost_blk_update_size() to return zero or error and move 'changed'
to a separate argument
 - changed lseek to BLKGETSIZE64 (we already use BLKROGET anyway)

v3:
 - make vhost_blk_update_size() return the changed flag directly
 - use ERRP_GUARD() for reliable error propagation

v4:
 - document the default geometry values

 hw/block/vhost-blk.c          | 126 ++++++++++++++++++++--------------
 include/hw/virtio/vhost-blk.h |   5 +-
 2 files changed, 79 insertions(+), 52 deletions(-)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index 20c2f41691..a517c5e22b 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -25,22 +25,17 @@
 #include "linux-headers/linux/vhost.h"
 #include <sys/ioctl.h>
 #include <linux/fs.h>
-#include "include/block/block_int-common.h"
 #include "system/runstate.h"
 
 static int vhost_blk_start(VirtIODevice *vdev)
 {
     VHostBlk *s = VHOST_BLK(vdev);
     struct vhost_vring_file backend;
-    int ret, i, nworkers, *fd;
+    int ret, i, nworkers;
     BusState *qbus = BUS(qdev_get_parent_bus(DEVICE(vdev)));
     VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
     char serial[VIRTIO_BLK_ID_BYTES] = {0};
 
-    bdrv_graph_rdlock_main_loop();
-    fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
-    bdrv_graph_rdunlock_main_loop();
-
     if (!k->set_guest_notifiers) {
         error_report("vhost-blk: binding does not support guest notifiers");
         return -ENOSYS;
@@ -92,7 +87,7 @@ static int vhost_blk_start(VirtIODevice *vdev)
 
     memset(&backend, 0, sizeof(backend));
     backend.index = 0;
-    backend.fd = *fd;
+    backend.fd = s->backend_fd;
     if (ioctl(s->vhostfd, VHOST_BLK_SET_BACKEND, &backend)) {
         error_report("vhost-blk: unable to set backend");
         ret = -errno;
@@ -208,29 +203,71 @@ static void vhost_blk_vm_state(void *opaque, bool running, RunState state)
     }
 }
 
-static void vhost_blk_resize_cb(void *opaque)
+static bool vhost_blk_update_size(VHostBlk *s, Error **errp)
 {
-    VirtIODevice *vdev = opaque;
+    BlockConf *conf = &s->conf.conf;
+    uint64_t length;
+    bool changed;
+
+    if (ioctl(s->backend_fd, BLKGETSIZE64, &length) < 0) {
+        error_setg_errno(errp, errno,
+                         "vhost-blk: unable to determine size of '%s'",
+                         s->conf.devpath);
+        return false;
+    }
 
-    assert(qemu_get_current_aio_context() == qemu_get_aio_context());
-    virtio_notify_config(vdev);
+    changed = s->length != length;
+    s->length = length;
+
+    /* Default geometry mimicking hw/block/hd-geometry.c */
+    conf->heads = 16;
+    conf->secs = 63;
+    conf->cyls = s->length / BDRV_SECTOR_SIZE /
+                 (conf->heads * conf->secs);
+    conf->cyls = MIN(MAX(conf->cyls, 2U), 16383U);
+
+    return changed;
 }
 
-static void vhost_blk_resize(void *opaque)
+static bool vhost_blk_open_backend(VHostBlk *s, Error **errp)
 {
-    VirtIODevice *vdev = VIRTIO_DEVICE(opaque);
+    ERRP_GUARD();
+    BlockConf *conf = &s->conf.conf;
+    struct stat st;
 
-    /*
-     * virtio_notify_config() needs to acquire the global mutex,
-     * so it can't be called from an iothread. Instead, schedule
-     * it to be run in the main context BH.
-     */
-    aio_bh_schedule_oneshot(qemu_get_aio_context(), vhost_blk_resize_cb, vdev);
-}
+    s->backend_fd = qemu_open(s->conf.devpath, O_RDWR, errp);
+    if (s->backend_fd < 0) {
+        error_prepend(errp, "vhost-blk: unable to open backend: ");
+        return false;
+    }
 
-static const BlockDevOps vhost_blk_block_ops = {
-    .resize_cb     = vhost_blk_resize,
-};
+    if (fstat(s->backend_fd, &st) < 0) {
+        error_setg_errno(errp, errno, "vhost-blk: unable to stat '%s'",
+                         s->conf.devpath);
+        goto fail;
+    }
+
+    if (!S_ISBLK(st.st_mode)) {
+        error_setg(errp, "vhost-blk: '%s' is not a block device",
+                   s->conf.devpath);
+        goto fail;
+    }
+
+    vhost_blk_update_size(s, errp);
+    if (*errp) {
+        goto fail;
+    }
+
+    conf->logical_block_size = BDRV_SECTOR_SIZE;
+    conf->physical_block_size = BDRV_SECTOR_SIZE;
+
+    return true;
+
+fail:
+    qemu_close(s->backend_fd);
+    s->backend_fd = -1;
+    return false;
+}
 
 static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
 {
@@ -240,13 +277,8 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
     struct vhost_virtqueue *vqs;
     int i, ret;
 
-    if (!conf->conf.blk) {
-        error_setg(errp, "vhost-blk: drive property not set");
-        return;
-    }
-
-    if (!blk_is_inserted(conf->conf.blk)) {
-        error_setg(errp, "vhost-blk: device needs media, but drive is empty");
+    if (!conf->devpath) {
+        error_setg(errp, "vhost-blk: devpath property must be set");
         return;
     }
 
@@ -274,17 +306,7 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
         return;
     }
 
-    if (!blkconf_apply_backend_options(&conf->conf,
-                                       !blk_supports_write_perm(conf->conf.blk),
-                                       true, errp)) {
-        return;
-    }
-
-    if (!blkconf_geometry(&conf->conf, NULL, 65535, 255, 255, errp)) {
-        return;
-    }
-
-    if (!blkconf_blocksizes(&conf->conf, errp)) {
+    if (!vhost_blk_open_backend(s, errp)) {
         return;
     }
 
@@ -313,8 +335,6 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
         goto cleanup;
     }
 
-    blk_set_dev_ops(s->conf.conf.blk, &vhost_blk_block_ops, s);
-
     ret = vhost_dev_init(&s->dev, (void *)((size_t)s->vhostfd),
                          VHOST_BACKEND_TYPE_KERNEL, 0, NULL);
     if (ret < 0) {
@@ -336,6 +356,10 @@ cleanup:
         close(s->vhostfd);
         s->vhostfd = -1;
     }
+    if (s->backend_fd >= 0) {
+        qemu_close(s->backend_fd);
+        s->backend_fd = -1;
+    }
     for (i = 0; i < conf->num_queues; i++) {
         virtio_del_queue(vdev, i);
     }
@@ -351,6 +375,10 @@ static void vhost_blk_device_unrealize(DeviceState *dev)
     qemu_del_vm_change_state_handler(s->mighand);
     vhost_blk_set_status(vdev, 0);
     vhost_dev_cleanup(&s->dev);
+    if (s->backend_fd >= 0) {
+        qemu_close(s->backend_fd);
+        s->backend_fd = -1;
+    }
     g_free(s->dev.vqs);
     virtio_cleanup(vdev);
 }
@@ -383,10 +411,6 @@ static uint64_t vhost_blk_get_features(VirtIODevice *vdev,
 
     virtio_add_feature(&features, VIRTIO_F_VERSION_1);
 
-    if (!blk_is_writable(s->conf.conf.blk)) {
-        virtio_add_feature(&features, VIRTIO_BLK_F_RO);
-    }
-
     if (s->conf.num_queues > 1) {
         virtio_add_feature(&features, VIRTIO_BLK_F_MQ);
     }
@@ -405,7 +429,9 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
     int64_t length;
     int blk_size = conf->logical_block_size;
 
-    blk_get_geometry(s->conf.conf.blk, &capacity);
+    length = s->length;
+    capacity = length / BDRV_SECTOR_SIZE;
+
     memset(&blkcfg, 0, sizeof(blkcfg));
     virtio_stq_p(vdev, &blkcfg.capacity, capacity);
     virtio_stl_p(vdev, &blkcfg.seg_max, s->conf.queue_size - 2);
@@ -413,7 +439,6 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
     virtio_stl_p(vdev, &blkcfg.blk_size, blk_size);
     blkcfg.geometry.heads = conf->heads;
 
-    length = blk_getlength(s->conf.conf.blk);
     if (length > 0 && length / conf->heads / conf->secs % blk_size) {
         unsigned short mask;
 
@@ -432,7 +457,7 @@ static void vhost_blk_update_config(VirtIODevice *vdev, uint8_t *config)
 }
 
 static const Property vhost_blk_properties[] = {
-    DEFINE_BLOCK_PROPERTIES(VHostBlk, conf.conf),
+    DEFINE_PROP_STRING("devpath", VHostBlk, conf.devpath),
     DEFINE_PROP_UINT16("num-queues", VHostBlk, conf.num_queues,
                        VHOST_BLK_AUTO_NUM_QUEUES),
     DEFINE_PROP_UINT16("queue-size", VHostBlk, conf.queue_size, 256),
@@ -478,6 +503,7 @@ static void vhost_blk_instance_init(Object *obj)
     VHostBlk *s = VHOST_BLK(obj);
 
     s->vhostfd = -1;
+    s->backend_fd = -1;
     device_add_bootindex_property(obj, &s->conf.conf.bootindex,
                                   "bootindex", "/disk@0,0",
                                   DEVICE(obj));
diff --git a/include/hw/virtio/vhost-blk.h b/include/hw/virtio/vhost-blk.h
index 0c7e212595..c194b421d9 100644
--- a/include/hw/virtio/vhost-blk.h
+++ b/include/hw/virtio/vhost-blk.h
@@ -14,7 +14,6 @@
 #include "standard-headers/linux/virtio_blk.h"
 #include "hw/block/block.h"
 #include "hw/virtio/vhost.h"
-#include "system/block-backend.h"
 
 #define TYPE_VHOST_BLK "vhost-blk"
 #define VHOST_BLK(obj) \
@@ -25,6 +24,7 @@
 
 typedef struct VhostBlkConf {
     BlockConf conf;
+    char *devpath;
     uint16_t num_queues;
     uint16_t queue_size;
     uint16_t num_threads;
@@ -37,10 +37,11 @@ typedef struct VHostBlk {
     VMChangeStateEntry *mighand;
     uint64_t host_features;
     uint64_t decided_features;
-    struct virtio_blk_config blkcfg;
     int vhostfd;
+    int backend_fd;
     struct vhost_dev dev;
     bool vhost_started;
+    uint64_t length;
 } VHostBlk;
 
 #endif
-- 
2.43.5


^ permalink raw reply	[relevance 23%]

* [QEMU HCI-8.0 PATCH v4 1/5] vhost-blk: improve vhost_blk_device_realize cleanup
  @ 2026-09-08 12:52 21% ` Andrey Zhadchenko
  2026-09-08 12:52 23% ` [QEMU HCI-8.0 PATCH v4 2/5] vhost-blk: change backend setup Andrey Zhadchenko
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 200+ results
From: Andrey Zhadchenko @ 2026-09-08 12:52 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

vhost_dev_init() does a lot of unexpected things:
 - closes the backend fd on failure
 - memsets dev as 0 (thus clearing dev->vqs)

Cleanup properly: don't double-close vhostfd, factor out vqs
so we do not leak it during cleanup.

Fixes: 0ff3cb5ff2 ("block: add vhost-blk backend")
https://virtuozzo.atlassian.net/browse/VSTOR-143437
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
v2:
 - old patch dropped

v3:
 - extend the double-close fix to preserve and free vqs after
vhost_dev_init() clears dev

 hw/block/vhost-blk.c | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index c52851fcf8..20c2f41691 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -237,6 +237,7 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
     VirtIODevice *vdev = VIRTIO_DEVICE(dev);
     VHostBlk *s = VHOST_BLK(vdev);
     VhostBlkConf *conf = &s->conf;
+    struct vhost_virtqueue *vqs;
     int i, ret;
 
     if (!conf->conf.blk) {
@@ -289,7 +290,8 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
 
     s->dev.nvqs = conf->num_queues;
     s->dev.max_queues = conf->num_queues;
-    s->dev.vqs = g_new0(struct vhost_virtqueue, s->dev.nvqs);
+    vqs = g_new0(struct vhost_virtqueue, s->dev.nvqs);
+    s->dev.vqs = vqs;
     s->dev.vq_index = 0;
 
     virtio_init(vdev, VIRTIO_ID_BLOCK, sizeof(struct virtio_blk_config));
@@ -318,6 +320,8 @@ static void vhost_blk_device_realize(DeviceState *dev, Error **errp)
     if (ret < 0) {
         error_setg(errp, "vhost-blk: vhost initialization failed: %s",
                 strerror(-ret));
+        /* vhost_dev_init() closes vhostfd on failure */
+        s->vhostfd = -1;
         goto cleanup;
     }
 
@@ -327,8 +331,11 @@ cleanup:
     if (s->mighand) {
             qemu_del_vm_change_state_handler(s->mighand);
     }
-    g_free(s->dev.vqs);
-    close(s->vhostfd);
+    g_free(vqs);
+    if (s->vhostfd >= 0) {
+        close(s->vhostfd);
+        s->vhostfd = -1;
+    }
     for (i = 0; i < conf->num_queues; i++) {
         virtio_del_queue(vdev, i);
     }
@@ -470,6 +477,7 @@ static void vhost_blk_instance_init(Object *obj)
 {
     VHostBlk *s = VHOST_BLK(obj);
 
+    s->vhostfd = -1;
     device_add_bootindex_property(obj, &s->conf.conf.bootindex,
                                   "bootindex", "/disk@0,0",
                                   DEVICE(obj));
-- 
2.43.5


^ permalink raw reply	[relevance 21%]

* Re: [QEMU HCI-8.0 PATCH v4 2/5] vhost-blk: change backend setup
  2026-09-08 12:52 23% ` [QEMU HCI-8.0 PATCH v4 2/5] vhost-blk: change backend setup Andrey Zhadchenko
@ 2026-09-08 13:34  0%   ` Andrey Drobyshev
  0 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-08 13:34 UTC (permalink / raw)
  To: Andrey Zhadchenko; +Cc: svt-core, den, andrey.drobyshev

On Tue, 08 Sep 2026 15:52:05 +0300, Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com> wrote:
> Previously we used very ugly and incapsulation-breaking assignment
> fd = blk_bs(s->conf.conf.blk)->file->bs->opaque;
> It is wrong in a many ways, so let's rework this.
> 
> Patch changes default `drive` to new `devpath` option so device fd
> is managed by vhost-blk itself. Unfortunately this way we need a
> bit more preparational work: finding out disk length, etc. Don't
> be too broad and just do the minimal work.
> Also drop generic block device properties along with the block
> node: the kernel module does all IO in terms of 512 sectors, so
> simply report 512 byte logical/physical block size to the guest.
> Existing vhost-blk setups will break, but it is expected.
> 
> [...]

Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

-- 
Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH VZ10 1/3] selftests/ve: Add namespace management helpers
  2026-09-07 19:14  6% ` [Devel] [PATCH VZ10 1/3] selftests/ve: Add namespace management helpers Vladimir Riabchun
@ 2026-09-09 10:39  0%   ` Vladimir Riabchun
  0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-09-09 10:39 UTC (permalink / raw)
  To: Konstantin Khorenko; +Cc: devel

Sashiko review + replies.

On 9/7/26 21:14, Vladimir Riabchun wrote:
> 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>
> ---
>   tools/testing/selftests/ve/ve_selftest.h | 118 +++++++++++++++++++++++++++++++
>   1 file changed, 118 insertions(+)
> 
> diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
> index c53bf7900d20..b3c5bf63f34d 100644
> --- a/tools/testing/selftests/ve/ve_selftest.h
> +++ b/tools/testing/selftests/ve/ve_selftest.h
> @@ -31,6 +31,124 @@
>   #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;

Sashiko says:
  If save_namespace_fds() is called twice on the same namespace_ctx
  without calling close_namespace_fds() in between, does this line reset
  the mask and leak the previously opened file descriptors?

User is responsible for freeing resources correctly. The idea of this design
was to avoid additional initialization of ctx before calling it. With current
approach we can pass ctx with any stack garbage and it will work.

> +
> +	OPEN_NS(ve, CLONE_NEWVE);
> +	OPEN_NS(mnt, CLONE_NEWNS);
> +
> +	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(mnt, CLONE_NEWNS);
> +	JOIN_NS(ve, CLONE_NEWVE);
> +	return 0;
> +}

Sashiko says:
  In enter_namespaces(), the namespaces are restored in a fixed order.
  Could this cause issues if there is a dependency between the VE and
  MNT namespaces that requires a specific restoration order based on
  the flags provided?

At least we should keep the same order as in save_namespace_fds. Will fix
in v2.

> +
> +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 original one).
> + */
> +static inline int prepare_namespaces(struct namespace_ctx *ctx, int flags,
> +	int cgv2_fd, int ctid)
> +{
> +	struct namespace_ctx tmp_ctx;
> +
> +	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__);
> +			return -1;
> +		}
> +	}
> +
> +	if (unshare(flags) < 0) {
> +		fprintf(stderr, "%s: failed to unshare, flags %x: %s\n",
> +			__func__, flags, strerror(errno));
> +		return -1;
> +	}
> +
> +	if (save_namespace_fds(ctx, flags)) {
> +		fprintf(stderr, "%s: failed to save new namespaces, flags %x\n",
> +			__func__, flags);
> +		return -1;
> +	}

Sashiko says:
  In the error paths above, such as when enter_cgroup(), unshare(), or
  save_namespace_fds(ctx, flags) fails, are the file descriptors held
  in tmp_ctx leaked?

True, but honestly I don't think it matters a lot - failing here is a
critical error and the test should be aborted. But I'll fix in v2.

> +
> +	if (flags & CLONE_NEWVE) {
> +		if (enter_cgroup(cgv2_fd, 0) < 0) {
> +			fprintf(stderr, "%s: failed to restore root cgroup\n", __func__);
> +			return -1;
> +		}
> +	}
> +
> +	if (enter_namespaces(&tmp_ctx, flags) < 0) {
> +		fprintf(stderr, "%s: failed to restore old namespaces, flags %x\n",
> +			__func__, flags);
> +		return -1;
> +	}
> +
> +	close_namespace_fds(&tmp_ctx);
> +	return 0;
> +}

Sashiko says:
  If enter_namespaces(&tmp_ctx, flags) fails, does the process remain
  in an inconsistent namespace state for subsequent tests?
  Additionally, does this failure path leak the file descriptors
  stored in tmp_ctx?

Fair, will fix in v2.

> +
>   static inline int write_file_at(int dirfd, const char *path, const char *val)
>   {
>   	int fd, ret;
> 

-- 
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 2/3] selftests/ve: Add more helpers
  2026-09-07 19:14  6% ` [Devel] [PATCH VZ10 2/3] selftests/ve: Add more helpers Vladimir Riabchun
@ 2026-09-09 11:03  4%   ` Vladimir Riabchun
  0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-09-09 11:03 UTC (permalink / raw)
  To: Konstantin Khorenko; +Cc: devel

Sashiko review + replies.

On 9/7/26 21:14, Vladimir Riabchun wrote:
> 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>
> ---
>   tools/testing/selftests/ve/ve_selftest.h | 78 +++++++++++++++++++++++++++++---
>   1 file changed, 72 insertions(+), 6 deletions(-)
> 
> diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
> index b3c5bf63f34d..d35fec30c7b4 100644
> --- a/tools/testing/selftests/ve/ve_selftest.h
> +++ b/tools/testing/selftests/ve/ve_selftest.h
> @@ -163,6 +163,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[20];
> +
> +	snprintf(s, sizeof(s), "%llu", val);
> +	return write_file_at(dirfd, path, s);
> +}

Sashiko says:
  Could this buffer be too small? A 64-bit unsigned integer can have up to
  20 digits. With the null terminator, the buffer s[] might need 21 bytes
  to avoid truncation.

Fair. Will be fixed in v2.

> +
>   static inline int read_file_at(int dirfd, const char *path, char *buf,
>   			       size_t buflen)
>   {
> @@ -193,19 +201,31 @@ static inline int read_u64_at(int dirfd, const char *path,
>   			      unsigned long long *out)
>   {
>   	char buf[32] = {0}, *end;
> -	int fd, ret;
> +	int ret;
>   
> -	fd = openat(dirfd, path, O_RDONLY);
> -	if (fd < 0)
> +	ret = read_file_at(dirfd, path, buf, sizeof(buf));
> +	if (ret <= 0)
>   		return -1;
>   
> -	ret = read(fd, buf, sizeof(buf) - 1);
> -	close(fd);
> +	errno = 0;
> +	*out = strtoull(buf, &end, 10);

Sashiko says:
  If read_file_at() fills the 32-byte buffer completely, does that remove
  the null terminator? If so, could strtoull() read out of bounds?

No, read_file_at is guaranteed to put \0 at the end of buffer.

> +	if (errno || end == buf)
> +		return -1;
> +	return 0;
> +}
> +
> +static inline int read_s32_at(int dirfd, const char *path,
> +			      int *out)
> +{
> +	char buf[32] = {0}, *end;
> +	int ret;
> +
> +	ret = read_file_at(dirfd, path, buf, sizeof(buf));
>   	if (ret <= 0)
>   		return -1;
>   
>   	errno = 0;
> -	*out = strtoull(buf, &end, 10);
> +	*out = strtol(buf, &end, 10);

Sashiko says:
  Does reading 32 bytes into a 32-byte buffer cause a similar out-of-bounds
  read issue here in read_s32_at() via strtol()?
  On 64-bit systems, strtol() returns a long. Can this lead to integer
  truncation when the result is stored in the 32-bit int pointer out?

1. No, same reason as above.
2. Maybe. It's worth adding range sanity check. Will do in v2.

>   	if (errno || end == buf)
>   		return -1;
>   	return 0;
> @@ -254,6 +274,52 @@ static inline int enter_cgroup(int cgv2_fd, int ctid)
>   	return ret;
>   }
>   
> +/*
> + * Run function in VE cgroup and new namespaces.
> + *
> + * Namespaces are provided via unshare_flags.
> + * CLONE_NEWVE flag is set by this function.
> + * Return values:
> + *  -  0 if function returns zero
> + *  - -1 if function returns negative value
> + *  -  1 if setup fails or function returns positive value
> + */
> +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 1;
> +	}
> +	if (pid == 0) {
> +		int ret;
> +
> +		if (enter_cgroup(cgv2_fd, ctid) < 0) {
> +			fprintf(stderr, "%s: enter_cgroup failed\n", __func__);
> +			_exit(255);
> +		}
> +		if (enter_namespaces(ctx, flags) < 0)
> +			_exit(255);
> +
> +		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) == 255)
> +		return 1;
> +	if (WEXITSTATUS(status))
> +		return -1;
> +	return 0;
> +
> +}
> +

Sashiko says:
  Does the return logic in run_in_ve() mask the root cause of failures?
  By returning 1 for waitpid failures, signal terminations, and
  WEXITSTATUS 255, it seems setup failures and process crashes are
  collapsed into a single value.

True. While values still will collapse due to return value truncation,
it is worth separating fork/waitpid errors and fn setup errors.
Will do this in v2.

>   /*
>    * 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.
> 

-- 
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 4%]

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

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>
---
 tools/testing/selftests/ve/ve_selftest.h | 123 +++++++++++++++++++++++++++++++
 1 file changed, 123 insertions(+)

diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index c53bf7900d20..ee960f7db0b4 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -31,6 +31,129 @@
 #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);
+
+	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);
+	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 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;
+
+	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);
+	return ret;
+}
+
 static inline int write_file_at(int dirfd, const char *path, const char *val)
 {
 	int fd, ret;

-- 
2.47.1

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

^ permalink raw reply	[relevance 6%]

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

This doesn't bring a lot of value and makes tests more complex.
To enter VE namespace we need to actually start VE.

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.

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

Feature: ve selftests
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
 kernel/ve/ve_namespace.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/kernel/ve/ve_namespace.c b/kernel/ve/ve_namespace.c
index 3dbe0dde2645..879a7863e33f 100644
--- a/kernel/ve/ve_namespace.c
+++ b/kernel/ve/ve_namespace.c
@@ -227,7 +227,8 @@ 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("Switching to a non-running ve %s.\n",
+			ve_name(ve_ns->ve));
 
 	/*
 	 * Don't need to put_ve_ns(nsset->ve_ns) here,

-- 
2.47.1

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

^ permalink raw reply	[relevance 4%]

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

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>
---
 tools/testing/selftests/ve/ve_selftest.h | 85 +++++++++++++++++++++++++++++---
 1 file changed, 78 insertions(+), 7 deletions(-)

diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index ee960f7db0b4..e88293a39d34 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -168,6 +168,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)
 {
@@ -198,14 +206,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;
 
@@ -216,6 +219,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)
 {
@@ -259,6 +281,55 @@ static inline int enter_cgroup(int cgv2_fd, int ctid)
 	return ret;
 }
 
+/*
+ * Run function in VE cgroup and new namespaces.
+ *
+ * Namespaces are provided via unshare_flags.
+ * CLONE_NEWVE flag is set by this function.
+ * 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.47.1

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

^ permalink raw reply	[relevance 5%]

* Re: [Devel] [PATCH VZ10 v2 2/3] selftests/ve: Add more helpers
  2026-09-09 12:14  5% ` [Devel] [PATCH VZ10 v2 2/3] selftests/ve: Add more helpers Vladimir Riabchun
@ 2026-09-09 14:33  0%   ` Vasileios Almpanis
  0 siblings, 0 replies; 200+ results
From: Vasileios Almpanis @ 2026-09-09 14:33 UTC (permalink / raw)
  To: Vladimir Riabchun; +Cc: devel

> 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>
>
> diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
> index ee960f7db0b4..e88293a39d34 100644
> --- a/tools/testing/selftests/ve/ve_selftest.h
> +++ b/tools/testing/selftests/ve/ve_selftest.h
> @@ -168,6 +168,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)
>  {
> @@ -198,14 +206,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;
>  
> @@ -216,6 +219,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)
>  {
> @@ -259,6 +281,55 @@ static inline int enter_cgroup(int cgv2_fd, int ctid)
>  	return ret;
>  }
>  
> +/*
> + * Run function in VE cgroup and new namespaces.
> + *
> + * Namespaces are provided via unshare_flags.

The documentation here is a bit weird. There is no unshare_flags
just flags and they are not passed to unshare but to enter_namespaces.

-- 
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 v2 1/3] selftests/ve: Add namespace management helpers
  2026-09-09 12:14  6% ` [Devel] [PATCH VZ10 v2 1/3] selftests/ve: Add namespace management helpers Vladimir Riabchun
@ 2026-09-09 14:33  0%   ` Vasileios Almpanis
  0 siblings, 0 replies; 200+ results
From: Vasileios Almpanis @ 2026-09-09 14:33 UTC (permalink / raw)
  To: Vladimir Riabchun; +Cc: devel

> 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>
>
> diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
> index c53bf7900d20..ee960f7db0b4 100644
> --- a/tools/testing/selftests/ve/ve_selftest.h
> +++ b/tools/testing/selftests/ve/ve_selftest.h
> @@ -31,6 +31,129 @@
>  #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) {                                                \

Parenthesise the macro argument, checkpatch complaints about it
	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);

Add #undef OPEN_NS after the last call to not leak to all includers and
the same for JOIN_NS.

-- 
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 v2 3/3] ve_namespace: Allow entering non-running VE namespace
  2026-09-09 12:14  4% ` [Devel] [PATCH VZ10 v2 3/3] ve_namespace: Allow entering non-running VE namespace Vladimir Riabchun
@ 2026-09-09 14:33  0%   ` Vasileios Almpanis
  2026-09-09 18:56  0%     ` Vladimir Riabchun
  0 siblings, 1 reply; 200+ results
From: Vasileios Almpanis @ 2026-09-09 14:33 UTC (permalink / raw)
  To: Vladimir Riabchun; +Cc: devel

> This doesn't bring a lot of value and makes tests more complex.
> To enter VE namespace we need to actually start VE.
> 
> 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.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-144504
> 
> Feature: ve selftests
> Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
>
> diff --git a/kernel/ve/ve_namespace.c b/kernel/ve/ve_namespace.c
> index 3dbe0dde2645..879a7863e33f 100644
> --- a/kernel/ve/ve_namespace.c
> +++ b/kernel/ve/ve_namespace.c
> @@ -227,7 +227,8 @@ 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("Switching to a non-running ve %s.\n",

Maybe better print ve->veid instead of ve_name(). ve_offline() sets
ve->ve_name to NULL when the ve cgroup is removed, so we will see 
"ve (null)" in that case.

> +			ve_name(ve_ns->ve));

This change means that now a host task can enter the CT before the CT
has been started. A task can enter into the VE by two ways either
copy_ve_ns or by switch_ve_namespace both of which call ve_setup_task.
This function sets whether the task can be ptraced, core dumped etc
which will be skipped because ve_setup_task checks if ve is running
which it will not be. So here we break the invariant that tasks that
enter into the ve from host cannot be ptraced etc. I suggest to add
a flag to ve_setup_task entered which will be 1 for switch_ve_namespace
and 0 for copy_ve_ns

-- 
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 v2 3/3] ve_namespace: Allow entering non-running VE namespace
  2026-09-09 14:33  0%   ` Vasileios Almpanis
@ 2026-09-09 18:56  0%     ` Vladimir Riabchun
  0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-09-09 18:56 UTC (permalink / raw)
  To: Vasileios Almpanis; +Cc: devel



On 9/9/26 16:33, Vasileios Almpanis wrote:
>> This doesn't bring a lot of value and makes tests more complex.
>> To enter VE namespace we need to actually start VE.
>>
>> 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.
>>
>> https://virtuozzo.atlassian.net/browse/VSTOR-144504
>>
>> Feature: ve selftests
>> Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
>>
>> diff --git a/kernel/ve/ve_namespace.c b/kernel/ve/ve_namespace.c
>> index 3dbe0dde2645..879a7863e33f 100644
>> --- a/kernel/ve/ve_namespace.c
>> +++ b/kernel/ve/ve_namespace.c
>> @@ -227,7 +227,8 @@ 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("Switching to a non-running ve %s.\n",
> 
> Maybe better print ve->veid instead of ve_name(). ve_offline() sets
> ve->ve_name to NULL when the ve cgroup is removed, so we will see
> "ve (null)" in that case.

We don't initialize this value in tests, it is 0.
Actually, can we have this situation? Reaching ve_ns_install means
we grabbed a ve_ns reference in validate_nsset. How can we do ->css_offline
at the same time?

> 
>> +			ve_name(ve_ns->ve));
> 
> This change means that now a host task can enter the CT before the CT
> has been started. A task can enter into the VE by two ways either
> copy_ve_ns or by switch_ve_namespace both of which call ve_setup_task.
> This function sets whether the task can be ptraced, core dumped etc
> which will be skipped because ve_setup_task checks if ve is running
> which it will not be. So here we break the invariant that tasks that
> enter into the ve from host cannot be ptraced etc. I suggest to add
> a flag to ve_setup_task entered which will be 1 for switch_ve_namespace
> and 0 for copy_ve_ns> 

Good point. I'm not sure how real it is though. It should be someone
really nasty on the host trying to poison CT, but if this nasty person
is switching VE ns - it has CAP_SYS_ADMIN already.

It can protect us from bugs in vzctl though, so I think it is not completely
useless.
What if we drop VE_IS_RUNNING(ve) in ve_setup_task?

-- 
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 VZ10 v3 2/3] selftests/ve: Add more helpers
    2026-09-09 21:32  6% ` [Devel] [PATCH VZ10 v3 1/3] selftests/ve: Add namespace management helpers Vladimir Riabchun
@ 2026-09-09 21:32  5% ` Vladimir Riabchun
  2026-09-09 21:32  4% ` [Devel] [PATCH VZ10 v3 3/3] ve_namespace: Allow entering non-running VE namespace Vladimir Riabchun
  2 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-09-09 21:32 UTC (permalink / raw)
  To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel

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>
---
 tools/testing/selftests/ve/ve_selftest.h | 85 +++++++++++++++++++++++++++++---
 1 file changed, 78 insertions(+), 7 deletions(-)

diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index 22c5c8dfc2d1..bf9b89b59a6c 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -171,6 +171,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)
 {
@@ -201,14 +209,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;
 
@@ -219,6 +222,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)
 {
@@ -262,6 +284,55 @@ 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.47.1

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

^ permalink raw reply	[relevance 5%]

* [Devel] [PATCH VZ10 v3 3/3] ve_namespace: Allow entering non-running VE namespace
    2026-09-09 21:32  6% ` [Devel] [PATCH VZ10 v3 1/3] selftests/ve: Add namespace management helpers Vladimir Riabchun
  2026-09-09 21:32  5% ` [Devel] [PATCH VZ10 v3 2/3] selftests/ve: Add more helpers Vladimir Riabchun
@ 2026-09-09 21:32  4% ` Vladimir Riabchun
  2 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-09-09 21:32 UTC (permalink / raw)
  To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel

This doesn't bring a lot of value and makes tests more complex.
To enter VE namespace we need to actually start VE.

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.

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

Feature: ve selftests
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
 kernel/ve/ve_namespace.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/kernel/ve/ve_namespace.c b/kernel/ve/ve_namespace.c
index 3dbe0dde2645..879a7863e33f 100644
--- a/kernel/ve/ve_namespace.c
+++ b/kernel/ve/ve_namespace.c
@@ -227,7 +227,8 @@ 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("Switching to a non-running ve %s.\n",
+			ve_name(ve_ns->ve));
 
 	/*
 	 * Don't need to put_ve_ns(nsset->ve_ns) here,

-- 
2.47.1

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

^ permalink raw reply	[relevance 4%]

* [Devel] [PATCH VZ10 v3 1/3] selftests/ve: Add namespace management helpers
  @ 2026-09-09 21:32  6% ` Vladimir Riabchun
  2026-09-09 21:32  5% ` [Devel] [PATCH VZ10 v3 2/3] selftests/ve: Add more helpers Vladimir Riabchun
  2026-09-09 21:32  4% ` [Devel] [PATCH VZ10 v3 3/3] ve_namespace: Allow entering non-running VE namespace Vladimir Riabchun
  2 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-09-09 21:32 UTC (permalink / raw)
  To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel

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>
---
 tools/testing/selftests/ve/ve_selftest.h | 126 +++++++++++++++++++++++++++++++
 1 file changed, 126 insertions(+)

diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index c53bf7900d20..22c5c8dfc2d1 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -31,6 +31,132 @@
 #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 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;
+
+	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);
+	return ret;
+}
+
 static inline int write_file_at(int dirfd, const char *path, const char *val)
 {
 	int fd, ret;

-- 
2.47.1

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

^ permalink raw reply	[relevance 6%]

* [Devel] [PATCH VZ10 v8 6/7] ve: Introduce per-VE failcount
    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  9% ` 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
  2 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-09-09 21:35 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 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  9% ` [Devel] [PATCH VZ10 v8 6/7] ve: Introduce per-VE failcount 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%]

* [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  9% ` [Devel] [PATCH VZ10 v8 6/7] ve: Introduce per-VE failcount 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 v2] selftests: binderfs: skip the stress test without binderfs
@ 2026-09-10  3:10  4% Eva Kurchatova
  0 siblings, 0 replies; 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 v2 1/2] selftests: pstore: skip when no backend is registered
@ 2026-09-10  3:17  4% Eva Kurchatova
  0 siblings, 0 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: pci_endpoint: skip when the test device is absent
@ 2026-09-10  3:21  3% Eva Kurchatova
  0 siblings, 0 replies; 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%]

* [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%]

* [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 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%]

* 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%]

* [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: [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 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%]

* [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 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%]

* Re: [Devel] [PATCH VZ10 v8 6/7] ve: Introduce per-VE failcount
  2026-09-09 21:35  9% ` [Devel] [PATCH VZ10 v8 6/7] ve: Introduce per-VE failcount 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

> 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%]

* 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%]

* [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%]

* [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 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 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 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%]

* 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%]

* [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%]

* [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 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 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
  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

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 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
  2 siblings, 0 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 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
  0 siblings, 1 reply; 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 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
  0 siblings, 1 reply; 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%]

* [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%]

* [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 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 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 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 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 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 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 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%]

* [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%]

* 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%]

* 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: [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
  0 siblings, 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: [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 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
  0 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%]

* [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%]

Results 1-200 of ~800   | reverse | sort options + mbox downloads above
-- links below jump to the message on this page --
2026-08-02 11:40     [Devel] [PATCH VZ10 v5 0/9] Add per-VE failcount support Vladimir Riabchun
2026-08-02 11:40  3% ` [Devel] [PATCH VZ10 v5 3/9] ve/fs: Rework per-ve mount count Vladimir Riabchun
2026-08-02 11:40  8% ` [Devel] [PATCH VZ10 v5 7/9] ve: Introduce per-VE failcount Vladimir Riabchun
2026-08-07  9:25  0%   ` Vasileios Almpanis
2026-08-02 11:40  6% ` [Devel] [PATCH VZ10 v5 8/9] selftests/ve: Add more helpers Vladimir Riabchun
2026-08-02 11:40  5% ` [Devel] [PATCH VZ10 v5 9/9] selftests/ve: Add mount accounting selftest Vladimir Riabchun
2026-08-07 10:01  0%   ` Vasileios Almpanis
     [not found]     <20260630160639.3043321-1-eva.kurchatova@virtuozzo.com>
2026-08-04 11:15  7% ` [Devel] [PATCH vz10 v2] ve/vtty: fix use-after-free on concurrent tty close and reopen Vasileios Almpanis
2026-08-04 12:53  3% [Devel] [PATCH vz10 v8 1/1] fs: enforce container device-mount policy in the common mount path Vasileios Almpanis
2026-08-05 16:41  0% ` Pavel Tikhomirov
2026-08-06 15:49  3% ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
     [not found]     <20260731155337.1209007-1-den@openvz.org>
2026-08-05 11:20  3% ` [Devel] [PATCH RHEL10 COMMIT] ms/qede: sync udp_tunnel ports outside qede_lock in the recovery path Konstantin Khorenko
     [not found]     <20260706110002.1024515-1-khorenko@virtuozzo.com>
     [not found]     ` <20260706110002.1024515-8-khorenko@virtuozzo.com>
     [not found]       ` <970b9d91-59c9-4a6d-8494-2d58afcab4b8@virtuozzo.com>
2026-08-05 19:26  0%     ` [Devel] [PATCH vz10 07/24] blk-cbt: don't WARN on a user-supplied ABI version mismatch Konstantin Khorenko
     [not found]     <20260706110002.1024515-17-khorenko@virtuozzo.com>
2026-08-05 20:05  5% ` [Devel] [PATCH RHEL10 COMMIT] drivers/base/cpu: fix cpu/offline content inside a ve Konstantin Khorenko
     [not found]     <20260706110002.1024515-19-khorenko@virtuozzo.com>
2026-08-05 20:09  4% ` [Devel] [PATCH RHEL10 COMMIT] ve: fix NULL-deref / use-after-free in ve_create() error unwind Konstantin Khorenko
2026-08-09 18:18     [Devel] [PATCH vz10] selftests: build test modules against the kernel tree Eva Kurchatova
2026-08-10 16:29  3% ` Konstantin Khorenko
2026-08-11 11:49 19% [Devel] [PATCH VZ10] x86/bugs: Make Safe-RET robust against interrupt injection Pavel Tikhomirov
2026-08-11 14:48 19% ` [Devel] [PATCH RHEL10 COMMIT] ms/x86/bugs: " Konstantin Khorenko
2026-08-12 13:03     [Devel] [PATCH DRAFT vz10 0/5] Enable GRE ERSPAN inside Containers Konstantin Khorenko
2026-08-12 13:03  3% ` [Devel] [PATCH DRAFT vz10 1/5] Revert "ve/net/gre: Disable ERSPAN support in ip_gre module" Konstantin Khorenko
2026-08-12 20:19     [Devel] [PATCH VZ10 v2 00/10] dm-qcow2: improve discard and read-only merge handling Andrey Zhadchenko
2026-08-12 20:19     ` [Devel] [PATCH VZ10 v2 01/10] drivers/md/dm-qcow2: fix revert_cluster_alloc() for ext_l2 case Andrey Zhadchenko
2026-08-19 10:27  4%   ` Pavel Tikhomirov
2026-08-17  7:16  2% [Devel] [PATCH vz10 0/3] ve/fs: make mount ownership follow the mount namespace Mirian Shilakadze
2026-08-17  7:16  4% ` [Devel] [PATCH vz10 1/3] ve/fs: unlink the mount namespace on the copy_mnt_ns() error path Mirian Shilakadze
2026-08-26 15:38  4%   ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
2026-08-18 10:40  0% ` [Devel] [PATCH vz10 0/3] ve/fs: make mount ownership follow the mount namespace Vasileios Almpanis
2026-08-18 15:09     [Devel] [PATCH VZ10 0/5] vhost-blk: fix protocol handling and backend setup Andrey Zhadchenko
2026-08-18 15:09  7% ` [Devel] [PATCH VZ10 5/5] drivers/vhost/blk: rework queue/backend setup Andrey Zhadchenko
     [not found]     <20260625181637.1555685-1-eva.kurchatova@virtuozzo.com>
     [not found]     ` <20260625181637.1555685-3-eva.kurchatova@virtuozzo.com>
2026-08-18 16:53  4%   ` [Devel] [PATCH vz10 3/5] fixup! vhost/vsock: only refuse connection when guest has never been ready Konstantin Khorenko
2026-08-19  9:07     [Devel] [PATCH VZ10 v6 0/9] Add per-VE failcount support Vladimir Riabchun
2026-08-19  9:07  3% ` [Devel] [PATCH VZ10 v6 3/9] ve/fs: Rework per-ve mount count Vladimir Riabchun
2026-08-19 13:16  0%   ` Vasileios Almpanis
2026-08-19  9:07  8% ` [Devel] [PATCH VZ10 v6 7/9] ve: Introduce per-VE failcount Vladimir Riabchun
2026-08-19 13:16  0%   ` Vasileios Almpanis
2026-08-19  9:07  6% ` [Devel] [PATCH VZ10 v6 8/9] selftests/ve: Add more helpers Vladimir Riabchun
2026-08-19  9:07  5% ` [Devel] [PATCH VZ10 v6 9/9] selftests/ve: Add mount accounting selftest Vladimir Riabchun
2026-08-19 13:16  0%   ` Vasileios Almpanis
     [not found]     <20260626113435.2210877-1-eva.kurchatova@virtuozzo.com>
     [not found]     ` <4492431c-87f8-4de6-9221-4e57107113bc@virtuozzo.com>
2026-08-19 14:12  0%   ` [Devel] [PATCH vz10] selftests/damon: add script dir to sys.path for PYTHONSAFEPATH compatibility Konstantin Khorenko
2026-08-19 14:16  3% ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
     [not found]     <20260706110002.1024515-9-khorenko@virtuozzo.com>
2026-08-19 16:25 13% ` [Devel] [PATCH vz10 v2] ve: downgrade the trusted exec/mmap denial from WARN to pr_warn Konstantin Khorenko
2026-08-26 16:49  0%   ` Konstantin Khorenko
2026-08-21 15:03     [Devel] [PATCH vz10] selftests/uevent: give the netlink socket a usable receive buffer Eva Kurchatova
2026-08-24 13:09  3% ` [Devel] [PATCH vz10 v2] selftests/uevent: do not fail on a netlink receive buffer overrun Konstantin Khorenko
2026-08-26 16:49  0%   ` Konstantin Khorenko
2026-08-26 19:32  0%   ` Eva Kurchatova (Virtuozzo)
2026-08-27 12:28  4%   ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
2026-08-21 15:13 13% [Devel] [PATCH vz10] selftests/damon: wait for the merge to apply the new max_nr_regions Eva Kurchatova
2026-08-31 23:22  0% ` Eva Kurchatova (Virtuozzo)
2026-08-21 15:32     [Devel] [PATCH vz10] selftests: ve_printk: match the conntrack overflow message Eva Kurchatova
2026-08-24 14:53  3% ` [Devel] [PATCH vz10 v2] " Konstantin Khorenko
2026-08-24 14:54  3%   ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
2026-08-21 16:36     [Devel] [PATCH vz10 00/32] Fix the VZ kernel build so that KUnit can run Konstantin Khorenko
2026-08-21 16:36  5% ` [Devel] [PATCH vz10 10/32] mm, proc: build the /proc/meminfo virtualization only with CONFIG_VE Konstantin Khorenko
2026-08-21 16:42  5%   ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
2026-08-21 16:37  9% ` [Devel] [PATCH vz10 29/32] ms/pcmcia: cistpl: Constify 'struct bin_attribute' Konstantin Khorenko
2026-08-21 16:42  9%   ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
2026-08-21 16:37  4% ` [Devel] [PATCH vz10 32/32] kunit: add the script dir to sys.path for PYTHONSAFEPATH compatibility Konstantin Khorenko
2026-08-21 16:42  3%   ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
2026-08-21 16:41  4% [Devel] [PATCH vz10 1/3] selftests: net: Adapt ethtool mq tests to fix in qdisc graft Eva Kurchatova
2026-08-24 13:36  0% ` Konstantin Khorenko
2026-08-24 14:36  3% ` [Devel] [PATCH RHEL10 COMMIT] ms/selftests: " Konstantin Khorenko
2026-08-24 13:54     [Devel] [PATCH VZ10 v7 0/9] Add per-VE failcount support Vladimir Riabchun
2026-08-24 13:54  3% ` [Devel] [PATCH VZ10 v7 3/9] ve/fs: Rework per-ve mount count Vladimir Riabchun
2026-08-24 13:54  7% ` [Devel] [PATCH VZ10 v7 7/9] ve: Introduce per-VE failcount Vladimir Riabchun
2026-08-28 16:52  4%   ` Pavel Tikhomirov
2026-08-28 17:32  0%     ` Vladimir Riabchun
2026-08-24 13:54  6% ` [Devel] [PATCH VZ10 v7 8/9] selftests/ve: Add more helpers Vladimir Riabchun
2026-08-24 13:54  5% ` [Devel] [PATCH VZ10 v7 9/9] selftests/ve: Add mount accounting selftest Vladimir Riabchun
2026-08-24 14:36  4% [Devel] [PATCH RHEL10 COMMIT] ms/selftests: net: Adapt ethtool mq tests to fix in qdisc graft Konstantin Khorenko
2026-08-24 15:59     [Devel] [PATCH VZ10 v2 0/5] vhost-blk: fix protocol handling and backend setup Andrey Zhadchenko
2026-08-24 15:59  7% ` [Devel] [PATCH VZ10 v2 5/5] drivers/vhost/blk: rework queue/backend setup Andrey Zhadchenko
2026-08-25 12:51     [Devel] [PATCH VZ10 v3 0/5] vhost-blk: fix protocol handling and backend setup Andrey Zhadchenko
2026-08-25 12:51  7% ` [Devel] [PATCH VZ10 v3 5/5] drivers/vhost/blk: rework queue/backend setup Andrey Zhadchenko
2026-08-25 13:57  7%   ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
2026-08-25 13:29     [Devel] [PATCH vz10 0/2] fs/kernfs, ve: stop a container lookup unmounting host filesystems Mirian Shilakadze
2026-08-25 13:29  5% ` [Devel] [PATCH vz10 2/2] selftests/ve: check that hiding an entry does not unmount it Mirian Shilakadze
2026-08-25 15:40  0%   ` Pavel Tikhomirov
2026-08-26 11:04     [Devel] [PATCH vz10 v2 0/2] fs/kernfs, ve: stop a container lookup unmounting host filesystems Mirian Shilakadze
2026-08-26 11:04  5% ` [Devel] [PATCH vz10 v2 2/2] selftests/ve: check that hiding an entry does not unmount it Mirian Shilakadze
2026-08-26 16:15  5%   ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
2026-08-26 14:44  5% [Devel] [PATCH VZ10] b4: ship project defaults in .b4-config Vasileios Almpanis
2026-08-26 15:15  0% ` Pavel Tikhomirov
2026-08-26 16:42  5% ` [Devel] [PATCH RHEL10 COMMIT] " Konstantin Khorenko
2026-08-27 12:37  3% [Devel] [PATCH VZ10] fs/fuse kio: track pending kRPC connect via state machine only Liu Kui
2026-08-28 17:13  0% ` Konstantin Khorenko
2026-09-01 20:59  0% ` Konstantin Khorenko
2026-08-28 11:15 12% [PATCH QEMU HCI-8.0] b4: ship project defaults in .b4-config Vasileios Almpanis
2026-08-28 11:16 12% Vasileios Almpanis
2026-08-28 11:40  4% ` Andrey Drobyshev
2026-08-31 15:16  3% [Devel] [PATCH 1/1] ms/virtio_ring: fix infinite loop in virtnet_poll_cleantx when device is broken Denis V. Lunev
2026-08-31 22:44  4% [Devel] [PATCH vz10] ms/selftests/sched_ext: flush stdout before test to avoid log spam Eva Kurchatova
2026-08-31 22:45  4% [Devel] [PATCH vz10 1/3] ms/selftests/posix_timers: Use CLOCK_THREAD_CPUTIME_ID for ITIMER_PROF measurements Eva Kurchatova
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  2% ` [Devel] [PATCH vz10 3/7] selftests: net: skip what this kernel and iproute2 do not have Eva Kurchatova
2026-08-31 22:48 16% ` [Devel] [PATCH vz10 7/7] selftests: net: let the bridged PMTU tests take the ICMP they ask for Eva Kurchatova
2026-08-31 23:13     [Devel] [PATCH vz10 v2 1/2] ms/selftests/damon/damon_nr_regions: set ops update for merge results check to 100ms Eva Kurchatova
2026-08-31 23:13  4% ` [Devel] [PATCH vz10 v2 2/2] ms/selftests/damon/damon_nr_regions: sort collected regiosn before checking with min/max boundaries Eva Kurchatova
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  7% ` [Devel] [PATCH vz10 3/3] selftests: drv-net: read the channel count over netlink Eva Kurchatova
2026-08-31 23:47  3% [Devel] [PATCH vz10] selftests: pci_endpoint: skip when the test device is absent Eva Kurchatova
2026-09-02 14:57  4% ` Konstantin Khorenko
2026-08-31 23:48  4% [Devel] [PATCH vz10] selftests: pstore: skip when no backend is registered Eva Kurchatova
2026-09-02 14:29  0% ` Konstantin Khorenko
2026-09-01  0:41  3% [Devel] [PATCH vz10] selftests: dma-buf: skip the huge page test without huge pages Eva Kurchatova
2026-09-03 12:31     [QEMU HCI-8.0 PATCH 0/5] vhost-blk change backend setup Andrey Zhadchenko
2026-09-03 12:32 24% ` [QEMU HCI-8.0 PATCH 2/5] vhost-blk: " Andrey Zhadchenko
2026-09-03 14:56  0%   ` Andrey Drobyshev
2026-09-03 15:27  0%     ` Andrey Zhadchenko
2026-09-03 15:34  0%       ` Andrey Drobyshev
2026-09-03 12:32 26% ` [QEMU HCI-8.0 PATCH 3/5] vhost-blk: add read-only flag Andrey Zhadchenko
2026-09-03 14:56  6%   ` Andrey Drobyshev
2026-09-03 12:32 20% ` [QEMU HCI-8.0 PATCH 4/5] vhost-blk: watch the device for resize events Andrey Zhadchenko
2026-09-03 12:32  3% ` [QEMU HCI-8.0 PATCH 5/5] vhost-blk: filter uevents in the kernel Andrey Zhadchenko
2026-09-03 20:25     [QEMU HCI-8.0 PATCH 0/7] qxl cursor use-after-free plus stability backports #VSTOR-144000 Denis V. Lunev
2026-09-03 20:25  8% ` [QEMU HCI-8.0 PATCH 3/7] hw/display/qxl: Fix mono cursor validation that can read past a cursor chunk #VSTOR-144000 Denis V. Lunev
2026-09-03 20:25  3% ` [QEMU HCI-8.0 PATCH 4/7] hw/display/qxl: fix TOCTOU in cursor chunk data_size handling #VSTOR-144000 Denis V. Lunev
2026-09-03 20:25  7% ` [QEMU HCI-8.0 PATCH 7/7] hw/display/qxl: validate primary surface stride against width #VSTOR-144000 Denis V. Lunev
2026-09-04 13:21     [QEMU HCI-8.0 PATCH v2 0/5] vhost-blk change backend setup Andrey Zhadchenko
2026-09-04 13:21 18% ` [QEMU HCI-8.0 PATCH v2 1/5] vhost-blk: do not double close vhostfd Andrey Zhadchenko
2026-09-04 15:33  6%   ` Andrey Drobyshev
2026-09-04 13:21 23% ` [QEMU HCI-8.0 PATCH v2 2/5] vhost-blk: change backend setup Andrey Zhadchenko
2026-09-04 15:33  0%   ` Andrey Drobyshev
2026-09-04 13:21 26% ` [QEMU HCI-8.0 PATCH v2 3/5] vhost-blk: add read-only flag Andrey Zhadchenko
2026-09-04 15:33  0%   ` Andrey Drobyshev
2026-09-04 13:21 20% ` [QEMU HCI-8.0 PATCH v2 4/5] vhost-blk: watch the device for resize events Andrey Zhadchenko
2026-09-04 15:33  6%   ` Andrey Drobyshev
2026-09-08  0:31  0%     ` Andrey Zhadchenko
2026-09-04 13:21 27% ` [QEMU HCI-8.0 PATCH v2 5/5] vhost-blk: preserve the uevent socket across cpr-exec Andrey Zhadchenko
2026-09-04 15:33  0%   ` Andrey Drobyshev
2026-09-04 19:04     [QEMU HCI-8.0 PATCH v2 00/13] usb-host: support CPR migration Andrey Drobyshev
2026-09-04 19:04 29% ` [QEMU HCI-8.0 PATCH v2 02/13] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
2026-09-04 19:04 13% ` [QEMU HCI-8.0 PATCH v2 03/13] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
2026-09-04 19:04 25% ` [QEMU HCI-8.0 PATCH v2 04/13] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
2026-09-04 19:04 25% ` [QEMU HCI-8.0 PATCH v2 05/13] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
2026-09-04 19:04 17% ` [QEMU HCI-8.0 PATCH v2 06/13] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
2026-09-04 19:04 21% ` [QEMU HCI-8.0 PATCH v2 07/13] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
2026-09-04 19:04 22% ` [QEMU HCI-8.0 PATCH v2 08/13] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-09-04 19:04 20% ` [QEMU HCI-8.0 PATCH v2 09/13] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
2026-09-04 19:04  5% ` [QEMU HCI-8.0 PATCH v2 11/13] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-09-04 19:04 20% ` [QEMU HCI-8.0 PATCH v2 13/13] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
2026-09-07 19:14     [Devel] [PATCH VZ10 0/3] Improvements for ve selftests Vladimir Riabchun
2026-09-07 19:14  6% ` [Devel] [PATCH VZ10 1/3] selftests/ve: Add namespace management helpers Vladimir Riabchun
2026-09-09 10:39  0%   ` Vladimir Riabchun
2026-09-07 19:14  6% ` [Devel] [PATCH VZ10 2/3] selftests/ve: Add more helpers Vladimir Riabchun
2026-09-09 11:03  4%   ` Vladimir Riabchun
2026-09-07 19:14  4% ` [Devel] [PATCH VZ10 3/3] ve_namespace: Allow entering non-running VE namespace Vladimir Riabchun
2026-09-08  9:12  0%   ` Pavel Tikhomirov
2026-09-08  1:04     [QEMU HCI-8.0 PATCH v3 0/5] vhost-blk change backend setup Andrey Zhadchenko
2026-09-08  1:04 21% ` [QEMU HCI-8.0 PATCH v3 1/5] vhost-blk: improve vhost_blk_device_realize cleanup Andrey Zhadchenko
2026-09-08  1:04 23% ` [QEMU HCI-8.0 PATCH v3 2/5] vhost-blk: change backend setup Andrey Zhadchenko
2026-09-08 11:11  0%   ` Andrey Drobyshev
2026-09-08  1:04 27% ` [QEMU HCI-8.0 PATCH v3 3/5] vhost-blk: add read-only flag Andrey Zhadchenko
2026-09-08  1:04 19% ` [QEMU HCI-8.0 PATCH v3 4/5] vhost-blk: watch the device for resize events Andrey Zhadchenko
2026-09-08  1:04 21% ` [QEMU HCI-8.0 PATCH v3 5/5] vhost-blk: preserve the uevent socket across cpr-exec Andrey Zhadchenko
2026-09-08 11:11  7%   ` Andrey Drobyshev
2026-09-08 12:52     [QEMU HCI-8.0 PATCH v4 0/5] vhost-blk change backend setup Andrey Zhadchenko
2026-09-08 12:52 21% ` [QEMU HCI-8.0 PATCH v4 1/5] vhost-blk: improve vhost_blk_device_realize cleanup Andrey Zhadchenko
2026-09-08 12:52 23% ` [QEMU HCI-8.0 PATCH v4 2/5] vhost-blk: change backend setup Andrey Zhadchenko
2026-09-08 13:34  0%   ` Andrey Drobyshev
2026-09-08 12:52 27% ` [QEMU HCI-8.0 PATCH v4 3/5] vhost-blk: add read-only flag Andrey Zhadchenko
2026-09-08 12:52 19% ` [QEMU HCI-8.0 PATCH v4 4/5] vhost-blk: watch the device for resize events Andrey Zhadchenko
2026-09-08 12:52 24% ` [QEMU HCI-8.0 PATCH v4 5/5] vhost-blk: preserve the uevent socket across cpr-exec Andrey Zhadchenko
2026-09-09 12:14     [Devel] [PATCH VZ10 v2 0/3] Improvements for ve selftests Vladimir Riabchun
2026-09-09 12:14  6% ` [Devel] [PATCH VZ10 v2 1/3] selftests/ve: Add namespace management helpers Vladimir Riabchun
2026-09-09 14:33  0%   ` Vasileios Almpanis
2026-09-09 12:14  5% ` [Devel] [PATCH VZ10 v2 2/3] selftests/ve: Add more helpers Vladimir Riabchun
2026-09-09 14:33  0%   ` Vasileios Almpanis
2026-09-09 12:14  4% ` [Devel] [PATCH VZ10 v2 3/3] ve_namespace: Allow entering non-running VE namespace Vladimir Riabchun
2026-09-09 14:33  0%   ` Vasileios Almpanis
2026-09-09 18:56  0%     ` Vladimir Riabchun
2026-09-09 21:32     [Devel] [PATCH VZ10 v3 0/3] Improvements for ve selftests Vladimir Riabchun
2026-09-09 21:32  6% ` [Devel] [PATCH VZ10 v3 1/3] selftests/ve: Add namespace management helpers Vladimir Riabchun
2026-09-09 21:32  5% ` [Devel] [PATCH VZ10 v3 2/3] selftests/ve: Add more helpers Vladimir Riabchun
2026-09-09 21:32  4% ` [Devel] [PATCH VZ10 v3 3/3] ve_namespace: Allow entering non-running VE namespace Vladimir Riabchun
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  9% ` [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-10  3:17  4% [Devel] [PATCH vz10 v2 1/2] selftests: pstore: skip when no backend is registered Eva Kurchatova
2026-09-10  3:21  3% [Devel] [PATCH vz10 v2] selftests: pci_endpoint: skip when the test device is absent Eva Kurchatova
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-15 17:22  6% ` [Devel] [PATCH VZ10 v9 8/8] selftests/ve: Add mount accounting selftest Vladimir Riabchun
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-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

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.