Re: [PATCH] s390/cpum_cf: Handle CPU hotplug add and delete

Thomas Richter <[email protected]>
Newsgroups dev.linux.lists.sashiko-reviews,org.kernel.vger.linux-s390
Organization IBM
Message-ID <[email protected]>
On 8/6/26 15:29, [email protected] wrote:
> Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
> 
> New issues:
> - [High] Unbounded reference count leak and memory leak on CPU hotplug for task events.
> - [High] Per-CPU events are permanently broken and suffer data loss when a CPU is offlined.
> - [High] Reference count overwrite on CPU online leads to Use-After-Free for per-CPU and hardware counter events.
> 
> Pre-existing issues:
> - [High] Data race in `cpum_cf_free_cpu` leads to Use-After-Free if interrupted by hardware counter alert.
> --
> 
> --- Patch [1]: [PATCH] s390/cpum_cf: Handle CPU hotplug add and delete ---
> commit 29becd857d549bdc0388325fd158df7d2f158812
> Author: Thomas Richter <[email protected]>
> 
> s390/cpum_cf: Handle CPU hotplug add and delete
>     
> The command 'perf stat -e cycles -- <command>' crashes the kernel when CPUs
> are hotplug added during that run. This commit tracks task-context users to
> ensure per-CPU structures are allocated when a new CPU is brought online.
> 
> Cc: <[email protected]> # v6.5+
> 
>> diff --git a/arch/s390/kernel/perf_cpum_cf.c b/arch/s390/kernel/perf_cpum_cf.c
>> index 2076ac22e2c49..9e8c3d2e71d57 100644
>> --- a/arch/s390/kernel/perf_cpum_cf.c
>> +++ b/arch/s390/kernel/perf_cpum_cf.c
> 
> [ ... ]
> 
>> @@ -206,20 +207,19 @@ static int cpum_cf_alloc_root(void)
>>  	return rc;
>>  }
>>  
>> -/* Free CPU counter data structure for a PMU */
>> +/* Free CPU counter data structure for a PMU. Called under mutex lock */
>>  static void cpum_cf_free_cpu(int cpu)
>>  {
>>  	struct cpu_cf_events *cpuhw;
>>  	struct cpu_cf_ptr *p;
>>  
>> -	mutex_lock(&pmc_reserve_mutex);
>>  	/*
>>  	 * When invoked via CPU hotplug handler, there might be no events
>>  	 * installed or that particular CPU might not have an
>>  	 * event installed. This anchor pointer can be NULL!
>>  	 */
>>  	if (!cpu_cf_root.cfptr)
>> -		goto out;
>> +		return;
>>  	p = per_cpu_ptr(cpu_cf_root.cfptr, cpu);
>>  	cpuhw = p->cpucf;
>>  	/*
>> @@ -227,15 +227,13 @@ static void cpum_cf_free_cpu(int cpu)
>>  	 * installed on that CPU, but on different CPUs.
>>  	 */
>>  	if (!cpuhw)
>> -		goto out;
>> +		return;
>>  
>>  	if (refcount_dec_and_test(&cpuhw->refcnt)) {
>>  		kfree(cpuhw);
>>  		p->cpucf = NULL;
> 
> [Severity: High]
> This is a pre-existing issue, but is it possible for an interrupt to
> dereference this pointer after it is freed?
> 
> Since cpum_cf_free_cpu() executes with interrupts enabled, if a CPUMF
> measurement alert interrupt triggers right after kfree(cpuhw) but before
> p->cpucf is set to NULL:
> 
> cpum_cf_free_cpu()
>   kfree(cpuhw);
>   
>   --- interrupt occurs ---
>   cpumf_measurement_alert()
>     this_cpu_cfhw() -> returns p->cpucf
>     ... accesses freed memory ...
> 
> Would it be safer to clear the pointer before calling kfree?

The interrupt handler is shared an protects against a NULL ptr:
        /*                                                                    
         * Measurement alerts are shared and might happen when the PMU        
         * is not reserved.  Ignore these alerts in this case.                
         */                                                                   
        cpuhw = this_cpu_cfhw();                                              
        if (!cpuhw)                                                           
                return;
> 
>>  	}
>>  	cpum_cf_free_root();
>> -out:
>> -	mutex_unlock(&pmc_reserve_mutex);
>>  }
> 
> [ ... ]
> 
>> @@ -290,9 +286,12 @@ static int cpum_cf_alloc(int cpu)
>>  	cpumask_var_t mask;
>>  	int rc;
>>  
>> +	mutex_lock(&pmc_reserve_mutex);
>>  	if (cpu == -1) {
>> -		if (!zalloc_cpumask_var(&mask, GFP_KERNEL))
>> -			return -ENOMEM;
>> +		if (!zalloc_cpumask_var(&mask, GFP_KERNEL)) {
>> +			rc = -ENOMEM;
>> +			goto out;
>> +		}
>>  		for_each_online_cpu(cpu) {
>>  			rc = cpum_cf_alloc_cpu(cpu);
>>  			if (rc) {
>> @@ -303,20 +302,27 @@ static int cpum_cf_alloc(int cpu)
>>  			cpumask_set_cpu(cpu, mask);
>>  		}
>>  		free_cpumask_var(mask);
>> +		if (!rc)
>> +			atomic_inc(&cpu_cf_root.tskcnt);
>>  	} else {
>>  		rc = cpum_cf_alloc_cpu(cpu);
>>  	}
>> +out:
>> +	mutex_unlock(&pmc_reserve_mutex);
>>  	return rc;
>>  }
>>  
>>  static void cpum_cf_free(int cpu)
>>  {
>> +	mutex_lock(&pmc_reserve_mutex);
>>  	if (cpu == -1) {
>>  		for_each_online_cpu(cpu)
>>  			cpum_cf_free_cpu(cpu);
>> +		atomic_dec(&cpu_cf_root.tskcnt);
> 
> [Severity: High]
> Does this loop potentially leak the per-CPU data structure if a CPU is
> offline?
> 
> If task events are closed while a CPU is offline, iterating only over
> for_each_online_cpu(cpu) skips that offline CPU.
> 
> Will this permanently leave the cpuhw structure and root references orphaned
> for that CPU?

If the CPU is offline, the removal of that per-CPU variable is handled by the
CPU hotplug remove path.

> 
>>  	} else {
>>  		cpum_cf_free_cpu(cpu);
>>  	}
>> +	mutex_unlock(&pmc_reserve_mutex);
>>  }
> 
> [ ... ]
> 
>> @@ -992,6 +1003,12 @@ static void cpumf_pmu_stop(struct perf_event *event, int flags)
>>  	struct hw_perf_event *hwc = &event->hw;
>>  	int i;
>>  
>> +	/* Might be zero when a per-task context event is active. Happens
>> +	 * when CPUs are made offline and process migration takes place.
>> +	 */
>> +	if (!cpuhw)
>> +		return;
>> +
> 
> [Severity: High]
> Could this early return cause us to lose accumulated performance counter data?
> 
> Since CPU hotplug teardown in s390 executes CPUHP_AP_PERF_S390_CF_ONLINE
> (state 209) before the generic perf core's CPUHP_AP_PERF_ONLINE (state 204),
> the per-CPU data structure has already been freed by cpum_cf_offline_cpu().
> 
> When the perf core eventually calls cpumf_pmu_stop() to stop the event, this
> check evaluates to true, aborting the function without updating the hardware
> counter state.
> 

True, this can cause some counter to miss the last read.
Should be addressed in a follow-on patch.

>>  	if (!(hwc->state & PERF_HES_STOPPED)) {
>>  		/* Decrement reference count for this counter set and if this
>>  		 * is the last used counter in the set, clear activation
> 
> [ ... ]
> 
>> @@ -1090,28 +1117,62 @@ static refcount_t cfset_opencnt = REFCOUNT_INIT(0);	/* Access count */
>>  static DEFINE_MUTEX(cfset_ctrset_mutex);
>>  
>>  /*
>> - * CPU hotplug handles only /dev/hwctr device.
>> - * For perf_event_open() the CPU hotplug handling is done on kernel common
>> - * code:
>> - * - CPU add: Nothing is done since a file descriptor can not be created
>> - *   and returned to the user.
>> - * - CPU delete: Handled by common code via pmu_disable(), pmu_stop() and
>> - *   pmu_delete(). The event itself is removed when the file descriptor is
>> - *   closed.
>> + * CPU hotplug handles /dev/hwctr device.
>> + *
>> + * For perf_event_open() the CPU hotplug handler needs to check the number
>> + * of per-task context events currently active. A per-task context event
>> + * needs per-CPU data structures. The scheduler might schedule the task on
>> + * the new CPU and then the CPUMF per-CPU infrastructure must be available.
>> + * Common code relies on that and calls cpumf_pmu_add(), cpumf_pmu_start(),
>> + * cpumf_pmu_stop() and cpumf_pmu_del() to install PMU backend functions on
>> + * the new CPU.
>> + *
>> + * If no per-task context event has been installed, the events are per-CPU
>> + * and do not care about a new CPU.
>>   */
>>  static int cfset_online_cpu(unsigned int cpu);
>>  
>>  static int cpum_cf_online_cpu(unsigned int cpu)
>>  {
>> -	int rc = 0;
>> +	int i, rc = 0;
>>  
>> -	/*
>> -	 * Ignore notification for perf_event_open().
>> -	 * Handle only /dev/hwctr device sessions.
>> -	 */
>>  	mutex_lock(&cfset_ctrset_mutex);
>> -	if (refcount_read(&cfset_opencnt)) {
>> +	/* Allocate per-CPU infrastructure when per-task context active. */
>> +	mutex_lock(&pmc_reserve_mutex);
>> +	if (atomic_read(&cpu_cf_root.tskcnt)) {
>>  		rc = cpum_cf_alloc_cpu(cpu);
> 
> [Severity: High]
> Are we failing to reallocate the data structure for CPU-bound events here?
> 
> Since per-CPU events do not increment the task context count (tskcnt), this
> check is bypassed.
> 
> Will subsequent attempts to interact with an existing CPU-bound event
> encounter a NULL pointer on this newly onlined CPU?

per-CPU bound events can not be bound to an offline CPU. New hotplug CPUs
are only used when a per-task event is active. Because the scheduler enables
performance measurement only when the event has per-process context.
When a new event is created with CPU-context, the per-CPU data is allocated
with perf_event_open() for that CPU.

> 
>> +		/* Adjust reference counts:
>> +		 * CPU X is offline
>> +		 *   perf_event_open() task event E1: tskcnt = 1, no cpuhw for CPU X
>> +		 *   perf_event_open() task event E2: tskcnt = 2
>> +		 *   CPU X set online
>> +		 *     cpum_cf_online_cpu()
>> +		 *       cpum_cf_alloc_cpu(X): cpuhw->refcnt = 1
>> +		 * E1 closed
>> +		 *   hw_perf_event_destroy()
>> +		 *     cpum_cf_free(-1)
>> +		 *       cpum_cf_free_cpu(X): refcnt 1 -> 0, kfree(cpuhw)
>> +		 * E2's task runs on CPU X
>> +		 *   cpumf_pmu_add(): this_cpu_cfhw() == NULL, -ENODEV
>> +		 *
>> +		 * If tskcnt > 1, adjust the reference counts to the number
>> +		 * of process-context tasks.
>> +		 */
>> +		if (!rc && atomic_read(&cpu_cf_root.tskcnt) > 1) {
>> +			struct cpu_cf_events *cpuhw;
>> +			struct cpu_cf_ptr *p;
>> +
>> +			p = per_cpu_ptr(cpu_cf_root.cfptr, cpu);
>> +			for (i = 1; i < atomic_read(&cpu_cf_root.tskcnt); ++i)
>> +				cpum_cf_alloc_root();
> 
> [Severity: High]
> Is there a risk of an unbounded reference count leak on the root structure
> here?
> 
> When a CPU goes offline, cpum_cf_offline_cpu() drops a single reference to
> cpuhw and cpu_cf_root.refcnt.
> 
> However, when brought back online with multiple active task events
> (tskcnt > 1), this loop adds tskcnt - 1 new references to
> cpu_cf_root.refcnt, causing it to grow indefinitely on every offline/online
> cycle.

No, when a CPU is hotplug added with multiple per-process tasks active, each
of these tasks will close the event and event deletion on a per-process task
uses for_each_online_cpu(). So when 3 per-process tasks were active at hotplug
add, 3 tasks will close the event and call cpumf_cf_free_cpu() 3 time. Which
is in sync with the reference count.

> 
>> +
>> +			cpuhw = p->cpucf;
>> +			refcount_set(&cpuhw->refcnt,
>> +				     atomic_read(&cpu_cf_root.tskcnt));
> 
> [Severity: High]
> Does forcefully setting the reference count here destroy references held by
> other active subsystems?
> 
> The cpuhw->refcnt field tracks all users on a CPU, including task events,
> per-CPU events, and /dev/hwctr sessions.
> 
> By overwriting it with tskcnt, wouldn't this erase any references held by
> other active sessions, causing a premature free of cpuhw later and leading
> to a Use-After-Free?

No the adjustment is done to match the per-process tasks which close events
with for_each_online_cpu() loops.

> 
>> +		}
>> +	}
>> +	mutex_unlock(&pmc_reserve_mutex);
>> +	if (refcount_read(&cfset_opencnt)) {
>>  		if (!rc)
>>  			cfset_online_cpu(cpu);
>>  	}
>> @@ -1123,20 +1184,11 @@ static int cfset_offline_cpu(unsigned int cpu);
>>  
>>  static int cpum_cf_offline_cpu(unsigned int cpu)
>>  {
>> -	/*
>> -	 * During task exit processing of grouped perf events triggered by CPU
>> -	 * hotplug processing, pmu_disable() is called as part of perf context
>> -	 * removal process. Therefore do not trigger event removal now for
>> -	 * perf_event_open() created events. Perf common code triggers event
>> -	 * destruction when the event file descriptor is closed.
>> -	 *
>> -	 * Handle only /dev/hwctr device sessions.
>> -	 */
>>  	mutex_lock(&cfset_ctrset_mutex);
>> -	if (refcount_read(&cfset_opencnt)) {
>> +	/* Handle /dev/hwctr device sessions */
>> +	if (refcount_read(&cfset_opencnt))
>>  		cfset_offline_cpu(cpu);
>> -		cpum_cf_free_cpu(cpu);
>> -	}
>> +	cpum_cf_free(cpu);
> 
> [Severity: High]
> Does unconditionally freeing the data structure here break active per-CPU
> events?

No the CPU is gone, we remove that CPU's data

> 
> As noted earlier, this function runs before the generic perf core can stop
> the event. Unconditionally dropping the refcount to 0 frees cpuhw while the
> hardware counter is still active.

As discussed above, the call backs protect themselves against NULL pointers.

> 
>>  	mutex_unlock(&cfset_ctrset_mutex);
>>  	return 0;
>>  }
> 


-- 
Thomas Richter, Dept 3303, IBM s390 Linux Development, Boeblingen, Germany
--
IBM Deutschland Research & Development GmbH

Vorsitzender des Aufsichtsrats: Wolfgang Wendt

Geschäftsführung: David Faller

Sitz der Gesellschaft: Böblingen / Registergericht: Amtsgericht Stuttgart, HRB 243294
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.