In landlock_restrict_sibling_threads(), when the calling thread is
interrupted while waiting for sibling threads to prepare, it executes
a recovery path.
Previously, this path included a wait_for_completion() call on
all_prepared to prevent a Use-After-Free of the local shared_ctx.
However, this wait is redundant. Exiting the main do-while loop
already leads to a bottom cleanup section that unconditionally waits
for all_finished. Therefore, replacing the wait with a simple break
is safe, prevents UAF, and correctly unblocks the remaining task_works.
Clean up the error path by breaking the loop and updating the
surrounding comments to accurately reflect the state machine.
Suggested-by: Günther Noack <gnoack3000@gmail.com>
Signed-off-by: Yihan Ding <dingyihan@uniontech.com>
---
Change in v3:
-No change in v3
Changes in v2:
- Replaced wait_for_completion(&shared_ctx.all_prepared) with a break
statement based on the realization that the bottom wait for 'all_finished'
already guards against UAF.
- Updated comments for clarity.
---
security/landlock/tsync.c | 18 +++++++++++-------
1 file changed, 11 insertions(+), 7 deletions(-)
diff --git a/security/landlock/tsync.c b/security/landlock/tsync.c
index 420fcfc2fe9a..9731ec7f329a 100644
--- a/security/landlock/tsync.c
+++ b/security/landlock/tsync.c
@@ -534,24 +534,28 @@ int landlock_restrict_sibling_threads(const struct cred *old_cred,
-ERESTARTNOINTR);
/*
- * Cancel task works for tasks that did not start running yet,
- * and decrement all_prepared and num_unfinished accordingly.
+ * Opportunistic improvement: try to cancel task works
+ * for tasks that did not start running yet. We do not
+ * have a guarantee that it cancels any of the enqueued
+ * task works (because task_work_run() might already have
+ * dequeued them).
*/
cancel_tsync_works(&works, &shared_ctx);
/*
- * The remaining task works have started running, so waiting for
- * their completion will finish.
+ * Break the loop with error. The cleanup code after the loop
+ * unblocks the remaining task_works.
*/
- wait_for_completion(&shared_ctx.all_prepared);
+ break;
}
}
} while (found_more_threads &&
!atomic_read(&shared_ctx.preparation_error));
/*
- * We now have all sibling threads blocking and in "prepared" state in the
- * task work. Ask all threads to commit.
+ * We now have either (a) all sibling threads blocking and in
+ * "prepared" state in the task work, or (b) the preparation error is
+ * set. Ask all threads to commit (or abort).
*/
complete_all(&shared_ctx.ready_to_commit);
--
2.51.0
On Thu, Feb 26, 2026 at 09:59:03AM +0800, Yihan Ding wrote: > In landlock_restrict_sibling_threads(), when the calling thread is > interrupted while waiting for sibling threads to prepare, it executes > a recovery path. > > Previously, this path included a wait_for_completion() call on > all_prepared to prevent a Use-After-Free of the local shared_ctx. > However, this wait is redundant. Exiting the main do-while loop > already leads to a bottom cleanup section that unconditionally waits > for all_finished. Therefore, replacing the wait with a simple break > is safe, prevents UAF, and correctly unblocks the remaining task_works. > > Clean up the error path by breaking the loop and updating the > surrounding comments to accurately reflect the state machine. > > Suggested-by: Günther Noack <gnoack3000@gmail.com> > Signed-off-by: Yihan Ding <dingyihan@uniontech.com> > --- > Change in v3: > -No change in v3 > > Changes in v2: > - Replaced wait_for_completion(&shared_ctx.all_prepared) with a break > statement based on the realization that the bottom wait for 'all_finished' > already guards against UAF. > - Updated comments for clarity. > --- > security/landlock/tsync.c | 18 +++++++++++------- > 1 file changed, 11 insertions(+), 7 deletions(-) > > diff --git a/security/landlock/tsync.c b/security/landlock/tsync.c > index 420fcfc2fe9a..9731ec7f329a 100644 > --- a/security/landlock/tsync.c > +++ b/security/landlock/tsync.c > @@ -534,24 +534,28 @@ int landlock_restrict_sibling_threads(const struct cred *old_cred, > -ERESTARTNOINTR); > > /* > - * Cancel task works for tasks that did not start running yet, > - * and decrement all_prepared and num_unfinished accordingly. > + * Opportunistic improvement: try to cancel task works > + * for tasks that did not start running yet. We do not > + * have a guarantee that it cancels any of the enqueued > + * task works (because task_work_run() might already have > + * dequeued them). > */ > cancel_tsync_works(&works, &shared_ctx); > > /* > - * The remaining task works have started running, so waiting for > - * their completion will finish. > + * Break the loop with error. The cleanup code after the loop > + * unblocks the remaining task_works. > */ > - wait_for_completion(&shared_ctx.all_prepared); > + break; > } > } > } while (found_more_threads && > !atomic_read(&shared_ctx.preparation_error)); > > /* > - * We now have all sibling threads blocking and in "prepared" state in the > - * task work. Ask all threads to commit. > + * We now have either (a) all sibling threads blocking and in > + * "prepared" state in the task work, or (b) the preparation error is > + * set. Ask all threads to commit (or abort). > */ > complete_all(&shared_ctx.ready_to_commit); > > -- > 2.51.0 > Reviewed-by: Günther Noack <gnoack3000@gmail.com>
© 2016 - 2026 Red Hat, Inc.