[PATCH v1] selftests/landlock: Test tsync interruption and cancellation paths

Günther Noack gnoack3000 at gmail.com
Sat Mar 14 21:10:23 UTC 2026


Hello Mickaël!

On Tue, Mar 10, 2026 at 08:04:15PM +0100, Mickaël Salaün wrote:
> Add tsync_interrupt test to exercise the signal interruption path in
> landlock_restrict_sibling_threads().  When a signal interrupts
> wait_for_completion_interruptible() while the calling thread waits for
> sibling threads to finish credential preparation, the kernel:
> 
> 1. Sets ERESTARTNOINTR to request a transparent syscall restart.
> 2. Calls cancel_tsync_works() to opportunistically dequeue task works
>    that have not started running yet.
> 3. Breaks out of the preparation loop, then unblocks remaining
>    task works via complete_all() and waits for them to finish.
> 4. Returns the error, causing abort_creds() in the syscall handler.
> 
> Specifically, cancel_tsync_works() in its entirety, the ERESTARTNOINTR
> error branch in landlock_restrict_sibling_threads(), and the
> abort_creds() error branch in the landlock_restrict_self() syscall
> handler are timing-dependent and not exercised by the existing tsync
> tests, making code coverage measurements non-deterministic.
> 
> The test spawns a signaler thread that rapidly sends SIGUSR1 to the
> calling thread while it performs landlock_restrict_self() with
> LANDLOCK_RESTRICT_SELF_TSYNC.  Since ERESTARTNOINTR causes a
> transparent restart, userspace always sees the syscall succeed.
> 
> This is a best-effort coverage test: the interruption path is exercised
> when the signal lands during the preparation wait, which depends on
> thread scheduling.  The test creates enough idle sibling threads (200)
> to ensure multiple serialized waves of credential preparation even on
> machines with many cores (e.g., 64), widening the window for the
> signaler.  Deterministic coverage would require wrapping the wait call
> with ALLOW_ERROR_INJECTION() and using CONFIG_FAIL_FUNCTION.
> 
> Cc: Günther Noack <gnoack at google.com>
> Cc: Justin Suess <utilityemal77 at gmail.com>
> Cc: Tingmao Wang <m at maowtm.org>
> Cc: Yihan Ding <dingyihan at uniontech.com>
> Signed-off-by: Mickaël Salaün <mic at digikod.net>
> ---
>  tools/testing/selftests/landlock/tsync_test.c | 91 ++++++++++++++++++-
>  1 file changed, 90 insertions(+), 1 deletion(-)
> 
> diff --git a/tools/testing/selftests/landlock/tsync_test.c b/tools/testing/selftests/landlock/tsync_test.c
> index 37ef0d2270db..2b9ad4f154f4 100644
> --- a/tools/testing/selftests/landlock/tsync_test.c
> +++ b/tools/testing/selftests/landlock/tsync_test.c
> @@ -6,9 +6,10 @@
>   */
>  
>  #define _GNU_SOURCE
> +#include <linux/landlock.h>
>  #include <pthread.h>
> +#include <signal.h>
>  #include <sys/prctl.h>
> -#include <linux/landlock.h>
>  
>  #include "common.h"
>  
> @@ -158,4 +159,92 @@ TEST(competing_enablement)
>  	EXPECT_EQ(0, close(ruleset_fd));
>  }
>  
> +static void signal_nop_handler(int sig)
> +{
> +}
> +
> +struct signaler_data {
> +	pthread_t target;
> +	volatile bool stop;
> +};
> +
> +static void *signaler_thread(void *data)
> +{
> +	struct signaler_data *sd = data;
> +
> +	while (!sd->stop)
> +		pthread_kill(sd->target, SIGUSR1);
> +
> +	return NULL;
> +}
> +
> +/*
> + * Number of idle sibling threads.  This must be large enough that even on
> + * machines with many cores, the sibling threads cannot all complete their
> + * credential preparation in a single parallel wave, otherwise the signaler
> + * thread has no window to interrupt wait_for_completion_interruptible().
> + * 200 threads on a 64-core machine yields ~3 serialized waves, giving the
> + * tight signal loop enough time to land an interruption.
> + */
> +#define NUM_IDLE_THREADS 200
> +
> +/*
> + * Exercises the tsync interruption and cancellation paths in tsync.c.
> + *
> + * When a signal interrupts the calling thread while it waits for sibling
> + * threads to finish their credential preparation
> + * (wait_for_completion_interruptible in landlock_restrict_sibling_threads),
> + * the kernel sets ERESTARTNOINTR, cancels queued task works that have not
> + * started yet (cancel_tsync_works), then waits for the remaining works to
> + * finish.  On the error return, syscalls.c aborts the prepared credentials.
> + * The kernel automatically restarts the syscall, so userspace sees success.
> + */
> +TEST(tsync_interrupt)
> +{
> +	size_t i;
> +	pthread_t threads[NUM_IDLE_THREADS];
> +	pthread_t signaler;
> +	struct signaler_data sd;
> +	struct sigaction sa = {};
> +	const int ruleset_fd = create_ruleset(_metadata);
> +
> +	disable_caps(_metadata);
> +
> +	/* Install a no-op SIGUSR1 handler so the signal does not kill us. */
> +	sa.sa_handler = signal_nop_handler;
> +	sigemptyset(&sa.sa_mask);
> +	ASSERT_EQ(0, sigaction(SIGUSR1, &sa, NULL));
> +
> +	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
> +
> +	for (i = 0; i < NUM_IDLE_THREADS; i++)
> +		ASSERT_EQ(0, pthread_create(&threads[i], NULL, idle, NULL));
> +
> +	/*
> +	 * Start a signaler thread that continuously sends SIGUSR1 to the
> +	 * calling thread.  This maximizes the chance of interrupting
> +	 * wait_for_completion_interruptible() in the kernel's tsync path.
> +	 */
> +	sd.target = pthread_self();
> +	sd.stop = false;
> +	ASSERT_EQ(0, pthread_create(&signaler, NULL, signaler_thread, &sd));
> +
> +	/*
> +	 * The syscall may be interrupted and transparently restarted by the
> +	 * kernel (ERESTARTNOINTR).  From userspace, it should always succeed.
> +	 */
> +	EXPECT_EQ(0, landlock_restrict_self(ruleset_fd,
> +					    LANDLOCK_RESTRICT_SELF_TSYNC));
> +
> +	sd.stop = true;
> +	ASSERT_EQ(0, pthread_join(signaler, NULL));
> +
> +	for (i = 0; i < NUM_IDLE_THREADS; i++) {
> +		ASSERT_EQ(0, pthread_cancel(threads[i]));
> +		ASSERT_EQ(0, pthread_join(threads[i], NULL));
> +	}
> +
> +	EXPECT_EQ(0, close(ruleset_fd));
> +}
> +
>  TEST_HARNESS_MAIN
> -- 
> 2.53.0

The purpose of a test is to catch errors, so I broke the
ERESTARTNOINTR error handling code path, but I could not get the test
to fail.  Did you manage to reproduce any of these bugs with it, by
any chance, and in what configuration did that work?  I tried with
both QEMU (a bit more) and UML (a bit less), but had no luck.

(Does this need to run in a loop like the Syzkaller-generated deadlock
reproducer, so that we have a chance of catching these bugs at all?)

–Günther



More information about the Linux-security-module-archive mailing list