[PATCH 0/3] perf: Fix SIGCHLD vs pause() race with short-lived workloads

Swapnil Sapkal posted 3 patches 14 hours ago
tools/perf/builtin-lock.c  | 20 ++++++++++++++++++--
tools/perf/builtin-sched.c | 30 ++++++++++++++++++++++++++----
2 files changed, 44 insertions(+), 6 deletions(-)
[PATCH 0/3] perf: Fix SIGCHLD vs pause() race with short-lived workloads
Posted by Swapnil Sapkal 14 hours ago
Some perf subcommands (sched stats, lock contention) use the pattern
of forking a workload child, calling evlist__start_workload() to uncork
it, and then calling pause() to wait for a signal (typically SIGCHLD
when the child exits, or SIGINT/SIGTERM from the user).

This pattern has a race condition: if the workload is very short-lived,
the child can exit and deliver SIGCHLD in the window between
evlist__start_workload() and pause(). Since pause() only returns when a
signal is received *while the process is suspended*, and SIGCHLD has
already been delivered and handled by the empty sighandler(), pause()
blocks indefinitely.

The fix uses the standard POSIX pattern for this class of bug:

1. Block SIGCHLD (via sigprocmask) before starting the workload.
   If the child exits, the signal remains pending rather than being
   delivered and lost.

2. Replace pause() with sigsuspend(&oldmask), which atomically
   unblocks SIGCHLD and suspends the process. There is no window
   where the signal can slip through unnoticed.

3. Restore the original signal mask after sigsuspend() returns.

SIGINT and SIGTERM are not blocked at any point, so Ctrl+C and
graceful termination continue to work exactly as before.

Three call sites are affected across two files:
  - perf_sched__schedstat_record() in builtin-sched.c
  - perf_sched__schedstat_live()   in builtin-sched.c
  - __cmd_contention()             in builtin-lock.c

The two pause() sites in builtin-kwork.c are NOT affected because they
do not register SIGCHLD or fork workload children; they only wait for
user-initiated SIGINT/SIGTERM.

Swapnil Sapkal (3):
  perf sched stats: Fix SIGCHLD race in schedstat_record()
  perf sched stats: Fix SIGCHLD race in schedstat_live()
  perf lock contention: Fix SIGCHLD race in __cmd_contention()

 tools/perf/builtin-lock.c  | 20 ++++++++++++++++++--
 tools/perf/builtin-sched.c | 30 ++++++++++++++++++++++++++----
 2 files changed, 44 insertions(+), 6 deletions(-)

-- 
2.43.0
Re: [PATCH 0/3] perf: Fix SIGCHLD vs pause() race with short-lived workloads
Posted by James Clark 9 hours ago

On 01/04/2026 7:41 am, Swapnil Sapkal wrote:
> Some perf subcommands (sched stats, lock contention) use the pattern
> of forking a workload child, calling evlist__start_workload() to uncork
> it, and then calling pause() to wait for a signal (typically SIGCHLD
> when the child exits, or SIGINT/SIGTERM from the user).
> 
> This pattern has a race condition: if the workload is very short-lived,
> the child can exit and deliver SIGCHLD in the window between
> evlist__start_workload() and pause(). Since pause() only returns when a
> signal is received *while the process is suspended*, and SIGCHLD has
> already been delivered and handled by the empty sighandler(), pause()
> blocks indefinitely.
> 
> The fix uses the standard POSIX pattern for this class of bug:
> 
> 1. Block SIGCHLD (via sigprocmask) before starting the workload.
>     If the child exits, the signal remains pending rather than being
>     delivered and lost.
> 
> 2. Replace pause() with sigsuspend(&oldmask), which atomically
>     unblocks SIGCHLD and suspends the process. There is no window
>     where the signal can slip through unnoticed.
> 
> 3. Restore the original signal mask after sigsuspend() returns.
> 
> SIGINT and SIGTERM are not blocked at any point, so Ctrl+C and
> graceful termination continue to work exactly as before.
> 
> Three call sites are affected across two files:
>    - perf_sched__schedstat_record() in builtin-sched.c
>    - perf_sched__schedstat_live()   in builtin-sched.c
>    - __cmd_contention()             in builtin-lock.c
> 
> The two pause() sites in builtin-kwork.c are NOT affected because they
> do not register SIGCHLD or fork workload children; they only wait for
> user-initiated SIGINT/SIGTERM.
> 
> Swapnil Sapkal (3):
>    perf sched stats: Fix SIGCHLD race in schedstat_record()
>    perf sched stats: Fix SIGCHLD race in schedstat_live()
>    perf lock contention: Fix SIGCHLD race in __cmd_contention()
> 
>   tools/perf/builtin-lock.c  | 20 ++++++++++++++++++--
>   tools/perf/builtin-sched.c | 30 ++++++++++++++++++++++++++----
>   2 files changed, 44 insertions(+), 6 deletions(-)
> 

Reviewed-by: James Clark <james.clark@linaro.org>