All Virtuozzo development lists (kernel + QEMU)
 help / color / mirror / Atom feed
From: Konstantin Khorenko <khorenko@virtuozzo.com>
Subject: Re: [Devel] [PATCH VZ10 v2 1/4] connector: annotate ve->cn with __rcu
Date: Tue, 25 Aug 2026 13:58:25 +0200	[thread overview]
Message-ID: <31c6c4fc-5890-4de1-aa54-8aeeea4518b0@virtuozzo.com> (raw)
In-Reply-To: <20260818-connectors-v2-1-88c5d9049e6f@virtuozzo.com>

On 8/18/26 17:10, Vasileios Almpanis wrote:
> The following patches will clear ve->cn on container stop and free the
> per-VE connector state only after an RCU grace period, so that the
> proc event delivery path can use it under rcu_read_lock() even when
> the reported task no longer pins the VE.
> 
> Prepare for that: annotate ve->cn with __rcu and switch all accesses
> to the RCU accessors so sparse can verify the protocol. All current
> users run either in the context of a task alive in the VE or from
> the VE start/stop hooks under ve->op_sem (or on ve0), so plain readers
> use rcu_dereference_check(ve->cn, 1) with a comment and the start/stop
> hooks use rcu_dereference_protected() with the proper lockdep
> condition.
> 
> No functional change intended
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-140421
> Signed-off-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
> 
> Feature: ve: ve generic structures
> ---
>  drivers/connector/cn_proc.c   | 48 +++++++++++++++++++++++++++++++------------
>  drivers/connector/connector.c | 43 +++++++++++++++++++++++++-------------
>  include/linux/ve.h            |  2 +-
>  3 files changed, 65 insertions(+), 28 deletions(-)
> 
...

> @@ -297,6 +304,7 @@ static int __maybe_unused cn_proc_show(struct seq_file *m, void *v)
>  static int cn_init_ve(void *data)
>  {
>  	struct ve_struct *ve = data;
> +	struct cn_private *cn;
>  	struct cn_dev *dev;
>  	struct netlink_kernel_cfg cfg = {
>  		.groups	= CN_NETLINK_USERS + 0xf,
> @@ -308,11 +316,12 @@ static int cn_init_ve(void *data)
>  	struct net *net;
>  	int err;
>  
> -	ve->cn = kzalloc(sizeof(*ve->cn), GFP_KERNEL);
> -	if (!ve->cn)
> +	cn = kzalloc(sizeof(*cn), GFP_KERNEL);
> +	if (!cn)
>  		return -ENOMEM;
> +	rcu_assign_pointer(ve->cn, cn);
>  
> -	dev = &ve->cn->cdev;
> +	dev = &cn->cdev;
>  
>  	/*
>  	 * This is a hook, hooks are called under a single lock, so ve_nsproxy will
> @@ -331,7 +340,7 @@ static int cn_init_ve(void *data)
>  		goto netlink_release;
>  	}
>  
> -	ve->cn->cn_already_initialized = 1;
> +	cn->cn_already_initialized = 1;
>  
>  	if (!proc_ve_create_single("connector", S_IRUGO, net->proc_net,
>  				cn_proc_show)) {
> @@ -353,18 +362,24 @@ static int cn_init_ve(void *data)
>  netlink_release:
>  	netlink_kernel_release(dev->nls);
>  free_cn:
> -	kfree(ve->cn);
> -	ve->cn = NULL;
> +	RCU_INIT_POINTER(ve->cn, NULL);
> +	kfree(cn);

  The attribution logic:

  - The error path itself (RCU_INIT_POINTER(ve->cn, NULL); kfree(cn) in cn_init_ve()) appeared in patch 1, where it
    faithfully preserves the base behavior (ve->cn = NULL; kfree) - the "no functional change" claim holds.
  - Before patch 4, the only possible reader of a container VE's ve->cn is a live task of that VE. While cn_init_ve() runs
    (VE start, under op_sem) there are no tasks in the VE yet - no reader can exist, so the immediate kfree is safe.
  - Patch 4 creates a new kind of reader: the exit event delivery with a pinned ve_struct, which can run after the VE has
    fully stopped - and therefore also concurrently with its restart (it is the very same ve_struct, the cgroup stays
    alive). Bisect-wise the crash only becomes reachable starting with patch 4 - hence the question sits in its
    review-inline.

  The scenario:

  CPU0: exiting task (old CT incarnation)        CPU1: vzctl start (restart of the same ve)
  =========================================      ========================================
  do_exit()
   ve = get_task_ve(tsk)      /* pins ve */
   exit_notify()              /* reaped, pid
                                 freed */
   <-- preempted; the CT manages to stop
       completely: cn_fini_ve() done,
       ve->cn == NULL -->
                                                 ve_start_container()  /* op_sem */
                                                  cn_init_ve()
                                                   cn = kzalloc(...)
                                                   rcu_assign_pointer(ve->cn, cn)  <-- published
   proc_exit_connector(tsk, &pids, ve)
    proc_event_connector_ve(task, ve, ...)
     rcu_read_lock()
     proc_event_num_listeners(ve)
      cn = rcu_dereference_check(ve->cn, 1)
      /* sees the FRESH cn from CPU1 */
                                                   netlink_kernel_create() -> fail
                                                  free_cn:
                                                   RCU_INIT_POINTER(ve->cn, NULL)
                                                   kfree(cn)          <-- NO grace period
      atomic_read(&cn->proc_event_num_listeners)
      *** use-after-free ***

  The reader dutifully sits under rcu_read_lock(), but unlike cn_proc_fini_ve() the error path does not wait for a grace
  period - patch 2's RCU contract ("everything reachable from ve->cn is freed only after a grace period") was not extended
  to this freeing site.

  That said, the right place to fix it is patch 2 rather than patch 4 - patch 2 owns the contract, and with the fix in place
  patch 4 lands on a fully safe base. The minimal variant, symmetric to cn_proc_fini_ve():


  --- a/drivers/connector/connector.c
  +++ b/drivers/connector/connector.c
  @@ static int cn_init_ve(void *data)
   free_cn:
        RCU_INIT_POINTER(ve->cn, NULL);
  +     /*
  +      * The pointer was published, so a pinned-VE exit event delivery
  +      * may already be looking at it under rcu_read_lock(): wait for
  +      * the readers before freeing, same as cn_proc_fini_ve() does.
  +      */
  +     synchronize_rcu();
        kfree(cn);
        goto net_unlock;

  The context is sleepable (GFP_KERNEL right nearby), and sleeping under op_sem is fine - ve_stop_ns()/cn_proc_fini_ve()
  already do exactly that.

  The architecturally cleaner alternative is to publish ve->cn only after the initialization fully succeeds (then a reader
  can never see a half-constructed state and the error path stays a plain kfree). But that is a bigger refactor:
  cn_proc_init_ve() and cn_add_callback_ve() dereference ve->cn themselves, so they would need to take cn as an argument.
  For this series the one-line synchronize_rcu() in the error path looks more appropriate.


>  	goto net_unlock;
>  }
>  
>  static void cn_fini_ve(void *data)
>  {
>  	struct ve_struct *ve = data;
> -	struct cn_dev *dev = get_cdev(ve);
> +	struct cn_private *cn;
> +	struct cn_dev *dev;
>  	struct net *net;
>  
> -	ve->cn->cn_already_initialized = 0;
> +	cn = rcu_dereference_protected(ve->cn,
> +				       lockdep_is_held(&ve->op_sem) ||
> +				       ve_is_super(ve));
> +	dev = &cn->cdev;
> +
> +	cn->cn_already_initialized = 0;
>  
>  	cn_proc_fini_ve(ve);
>  
> @@ -379,8 +394,8 @@ static void cn_fini_ve(void *data)
>  	cn_queue_free_dev(dev->cbdev);
>  	netlink_kernel_release(dev->nls);
>  
> -	kfree(ve->cn);
> -	ve->cn = NULL;
> +	RCU_INIT_POINTER(ve->cn, NULL);
> +	kfree(cn);
>  }
>  
>  #ifdef CONFIG_VE
> diff --git a/include/linux/ve.h b/include/linux/ve.h
> index cba827260d07..3334bd1e9517 100644
> --- a/include/linux/ve.h
> +++ b/include/linux/ve.h
> @@ -94,7 +94,7 @@ struct ve_struct {
>  	char			core_pattern[CORENAME_MAX_SIZE];
>  #endif
>  #ifdef CONFIG_CONNECTOR
> -	struct cn_private	*cn;
> +	struct cn_private __rcu	*cn;
>  #endif
>  
>  	struct kthread_worker	*kthreadd_worker;
> 


  reply	other threads:[~2026-08-25 11:58 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-18 15:10 [Devel] [PATCH VZ10 v2 0/4] connector: do not lose exit events for in-CT listeners Vasileios Almpanis
2026-08-18 15:10 ` [Devel] [PATCH VZ10 v2 1/4] connector: annotate ve->cn with __rcu Vasileios Almpanis
2026-08-25 11:58   ` Konstantin Khorenko [this message]
2026-08-18 15:10 ` [Devel] [PATCH VZ10 v2 2/4] connector: free the per-VE connector state after an RCU grace period Vasileios Almpanis
2026-08-25 11:47   ` Konstantin Khorenko
2026-08-25 12:43     ` Vasileios Almpanis
2026-08-18 15:10 ` [Devel] [PATCH VZ10 v2 3/4] connector: deliver per-VE proc events under an RCU read lock Vasileios Almpanis
2026-08-18 15:10 ` [Devel] [PATCH VZ10 v2 4/4] proc connector: pin task VE for the exit event notification Vasileios Almpanis

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=31c6c4fc-5890-4de1-aa54-8aeeea4518b0@virtuozzo.com \
    --to=khorenko@virtuozzo.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is 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.