[PATCH] landlock: fix LANDLOCK_SCOPE_SIGNAL bypass via F_SETOWN to invoker's pgid

Justin Suess utilityemal77 at gmail.com
Fri May 29 03:25:12 UTC 2026


On Thu, May 28, 2026 at 09:21:50PM +0000, hexlabsecurity at proton.me wrote:
> From 22a0086b44beaaef01883e047dd4a8b8bc3153e9 Mon Sep 17 00:00:00 2001
> From: Bryam Vargas <hexlabsecurity at proton.me>
> Date: Thu, 28 May 2026 01:30:00 -0500
> Subject: [PATCH] landlock: fix LANDLOCK_SCOPE_SIGNAL bypass via F_SETOWN to
>  invoker's pgid
> 
> A Landlock-restricted process can bypass LANDLOCK_SCOPE_SIGNAL on the
> SIGIO delivery path and deliver arbitrary signals (including SIGKILL via
> F_SETSIG) to non-Landlocked targets that share its pgid, by exploiting a
> producer-side cache-vs-live evaluation gap.
> 
> The SIGIO path in hook_file_send_sigiotask() consults a cached subject
> stored in landlock_file(file)->fown_subject at fcntl(F_SETOWN) time
> (via hook_file_set_fowner()), instead of evaluating the live Landlock
> domain of the invoking task at signal-send time. The capture is gated
> by control_current_fowner(), which returns false (skipping capture)
> when pid_task(fown->pid, fown->pid_type) is in current's thread group.
> 
> This is correct for PIDTYPE_TGID / PIDTYPE_PID, where the target is a
> single thread or thread-group leader sharing current's cred. It is
> unsafe for PIDTYPE_PGID and PIDTYPE_SID: when current is at the head
> of its pgid hlist -- the default placement after fork(),
> hlist_add_head_rcu() in kernel/fork.c -- pid_task(pgid, PIDTYPE_PGID)
> resolves to current itself, same_thread_group(current, current) is
> true, the capture is skipped, and fown_subject.domain stays NULL.
> 
> hook_file_send_sigiotask() then short-circuits at
> "if (!subject->domain) return 0;", allowing the kernel to fan the
> signal out to every member of the group, including tasks outside
> current's Landlock domain that the SCOPE_SIGNAL contract is supposed
> to protect.
> 
> The direct kill() path (hook_task_kill) is unaffected: it evaluates
> current's live domain on every call. Only the cached SIGIO path is
> broken.
> 
> Repro (ordinary unprivileged user; sandbox active in the child):
> 
>   int pfd[2]; pipe(pfd);
>   landlock_create_ruleset(&{.scoped = LANDLOCK_SCOPE_SIGNAL},
>                           sizeof(attr), 0);
>   prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
>   landlock_restrict_self(rfd, 0);
>   fcntl(pfd[0], F_SETSIG, SIGKILL);
>   fcntl(pfd[0], F_SETOWN, -getpgrp());           /* PIDTYPE_PGID */
>   fcntl(pfd[0], F_SETFL, O_ASYNC);
>   write(pfd[1], "X", 1);                         /* trigger SIGIO  */
>   /* every pgid member receives SIGKILL, including non-sandboxed
>    * parent / supervisor / sibling workers */
>
I was able to reproduce this on mic/next.

Great catch!

> Tighten control_current_fowner() to apply the thread-group exemption
> only when the target identifies a SINGLE task whose Landlock cred is
> necessarily shared with current (PIDTYPE_TGID, PIDTYPE_PID). For
> PIDTYPE_PGID and PIDTYPE_SID, always capture the current Landlock
> subject so the consumer's scope check runs against every member of
> the group at delivery time.
> 
> Empirically A/B-verified on a 6.12.90 lab kernel (same .config, only
> the patch hunk differs): pre-fix build exits with "BUG PRESENT --
> SCOPE_SIGNAL BYPASSED", post-fix build exits with "SANDBOX HELD".
> hook_task_kill's direct-kill enforcement and the intra-thread-group
> F_SETOWN cases continue to work post-patch.
> 
> Reported-by: Bryam Vargas <hexlabsecurity at proton.me>
> Signed-off-by: Bryam Vargas <hexlabsecurity at proton.me>
> ---
>  security/landlock/fs.c | 12 ++++++++++++
>  1 file changed, 12 insertions(+)
> 
> diff --git a/security/landlock/fs.c b/security/landlock/fs.c
> index c1ecfe239032..edaa52572cbd 100644
> --- a/security/landlock/fs.c
> +++ b/security/landlock/fs.c
> @@ -1909,6 +1909,18 @@ static bool control_current_fowner(struct fown_struct *const fown)
>  	if (!p)
>  		return true;
> 
> +	/*
> +	 * For PIDTYPE_PGID and PIDTYPE_SID, signal delivery fans out to
> +	 * every member of the group at SIGIO time. Even when pid_task()
> +	 * resolves to current itself (e.g., current is the pgid hlist
> +	 * head post-fork), non-current members of the group are still
> +	 * valid targets that must be checked by hook_file_send_sigiotask().
> +	 * Always capture the current subject for those types so the
> +	 * consumer scope check runs against the live fown_subject.
> +	 */
> +	if (fown->pid_type == PIDTYPE_PGID || fown->pid_type == PIDTYPE_SID)
> +		return true;
This seems right.

So basically we are failing to check the subject on fan-out
signals where type > PIDTYPE_TGID (ie PIDTYPE_PGID/SID).

But this fix seems good as is to me and closed the reproducer hole in my
test. Unless there are some edge cases I'm missing.

The commit message could use some cleanup and shortening. No need to
include the reproducer (though it was helpful) and the "BUG_PRESENT"/
"SANDBOX_HELD"/ AB testing stuff. Just explain the bug and what
it fixes :)

You can add the reproducer and stuff below the --- in the patch and
above the diffstat in the future to make it part of the git notes and
not the actual commit.

That way you can add anything else that doesn't belong in the actual
commit but is important for context.

This may need an erratum entry and a regression test in the future,
but that can be done seperately.

Again great job!

Tested-by: Justin Suess <utilityemal77 at gmail.com>
> +
>  	return !same_thread_group(p, current);
>  }
> 
> --
> 2.43.0
> 



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