Linux Security Modules development
 help / color / mirror / Atom feed
From: "Günther Noack" <gnoack3000@gmail.com>
To: Bryam Vargas <hexlabsecurity@proton.me>
Cc: "Mickaël Salaün" <mic@digikod.net>,
	"Günther Noack" <gnoack@google.com>,
	"Justin Suess" <utilityemal77@gmail.com>,
	"Christian Brauner" <brauner@kernel.org>,
	"Paul Moore" <paul@paul-moore.com>,
	"James Morris" <jmorris@namei.org>,
	"Serge E . Hallyn" <serge@hallyn.com>,
	linux-security-module@vger.kernel.org, stable@vger.kernel.org,
	linux-kernel@vger.kernel.org
Subject: Re: [PATCH v5 2/2] selftests/landlock: test SCOPE_SIGNAL on the SIGIO/fowner pgid path
Date: Fri, 5 Jun 2026 13:50:01 +0200	[thread overview]
Message-ID: <20260605.b1f90e8b16bd@gnoack.org> (raw)
In-Reply-To: <43370e89f7a896a583bf33d1cd171d02630e61bf.1780614610.git.hexlabsecurity@proton.me>

On Thu, Jun 04, 2026 at 11:17:05PM +0000, Bryam Vargas wrote:
> Add regression tests for the LANDLOCK_SCOPE_SIGNAL handling of the
> asynchronous SIGIO delivery path (fcntl(F_SETOWN)) with a process-group
> owner.
> 
> sigio_to_pgid_members covers the bypass: a sandboxed process at the head
> of its process group's PID hlist (the default after fork()) arms
> F_SETOWN(-pgrp) + O_ASYNC and triggers the fan-out; the in-domain owner
> must be signaled (proving the trigger fired) while the non-sandboxed
> member of the group, outside the domain, must not.
> 
> sigio_to_pgid_self covers the same-process guarantee: the owner is
> registered from a sandboxed non-leader thread, whose domain differs from
> the thread-group leader the kernel signals for a process-group owner.
> That leader belongs to the owner's own process and must still be signaled.
> 
> Without the fix the first test sees the out-of-domain member signaled and
> the second sees the owner's own leader denied.
> 
> Signed-off-by: Bryam Vargas <hexlabsecurity@proton.me>
> ---
>  .../selftests/landlock/scoped_signal_test.c   | 183 ++++++++++++++++++
>  1 file changed, 183 insertions(+)
> 
> diff --git a/tools/testing/selftests/landlock/scoped_signal_test.c b/tools/testing/selftests/landlock/scoped_signal_test.c
> index d8bf33417619..4359e0262dcf 100644
> --- a/tools/testing/selftests/landlock/scoped_signal_test.c
> +++ b/tools/testing/selftests/landlock/scoped_signal_test.c
> @@ -559,4 +559,187 @@ TEST_F(fown, sigurg_socket)
>  		_metadata->exit_code = KSFT_FAIL;
>  }
>  
> +/*
> + * Checks that LANDLOCK_SCOPE_SIGNAL is enforced on the asynchronous SIGIO
> + * delivery path (fcntl(F_SETOWN)) when the file owner is a process group.
> + *
> + * A sandboxed process sitting at the head of its process group's PID hlist
> + * (the default position right after fork()) used to escape the
> + * fcntl(F_SETOWN, -pgrp) domain recording: pid_task(pgrp, PIDTYPE_PGID)
> + * resolved to the process itself, so the same-thread-group exemption skipped
> + * recording its Landlock domain.  At SIGIO time that domain was then unset and
> + * the signal fanned out to every group member, including non-sandboxed
> + * processes outside the domain.
> + */
> +TEST(sigio_to_pgid_members)
> +{
> +	int trigger[2], sync_child[2];
> +	char buf;
> +	pid_t child;
> +	int status, i;
> +
> +	drop_caps(_metadata);
> +
> +	/*
> +	 * Isolates the test in its own process group so the SIGIO fan-out stays
> +	 * bounded to this parent and the child forked below.
> +	 */
> +	ASSERT_EQ(0, setpgid(0, 0));
> +
> +	/* The non-sandboxed parent is the protected (out-of-domain) target. */
> +	ASSERT_EQ(0, setup_signal_handler(SIGURG));
> +	signal_received = 0;
> +
> +	ASSERT_EQ(0, pipe2(trigger, O_CLOEXEC));
> +	ASSERT_EQ(0, pipe2(sync_child, O_CLOEXEC));
> +
> +	child = fork();
> +	ASSERT_LE(0, child);
> +	if (child == 0) {
> +		/*
> +		 * The child inherits the parent's new process group and, just
> +		 * attached with hlist_add_head_rcu(), is now the head of the
> +		 * pgid hlist: this is the case that used to skip the recording.
> +		 */
> +		EXPECT_EQ(0, close(sync_child[0]));
> +
> +		/* In-domain positive control: the child must be signaled. */
> +		ASSERT_EQ(0, setup_signal_handler(SIGURG));
> +		signal_received = 0;
> +
> +		create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
> +
> +		/* Owns the SIGIO source for the whole process group. */
> +		ASSERT_EQ(0, fcntl(trigger[0], F_SETSIG, SIGURG));
> +		ASSERT_EQ(0, fcntl(trigger[0], F_SETOWN, -getpgrp()));
> +		ASSERT_EQ(0, fcntl(trigger[0], F_SETFL, O_ASYNC));
> +
> +		/* Fans SIGURG out to every member of the process group. */
> +		ASSERT_EQ(1, write(trigger[1], ".", 1));
> +
> +		/*
> +		 * The sandboxed child is in its own domain and must always be
> +		 * signaled: this proves the SIGIO actually fired.
> +		 */
> +		for (i = 0; i < 1000 && !signal_received; i++)
> +			usleep(1000);
> +		EXPECT_EQ(1, signal_received);
> +
> +		ASSERT_EQ(1, write(sync_child[1], ".", 1));
> +		EXPECT_EQ(0, close(sync_child[1]));
> +
> +		_exit(_metadata->exit_code);
> +		return;
> +	}
> +	EXPECT_EQ(0, close(sync_child[1]));
> +	EXPECT_EQ(0, close(trigger[0]));
> +	EXPECT_EQ(0, close(trigger[1]));
> +
> +	/* Waits for the child to generate the SIGIO. */
> +	ASSERT_EQ(1, read(sync_child[0], &buf, 1));
> +	EXPECT_EQ(0, close(sync_child[0]));
> +
> +	/* Lets a delivered-but-pending signal run our handler, if any. */
> +	for (i = 0; i < 100 && !signal_received; i++)
> +		usleep(1000);
> +
> +	/*
> +	 * SCOPE_SIGNAL must block the fan-out to this non-sandboxed parent,
> +	 * which is outside the child's Landlock domain.  Before the fix the
> +	 * parent was signaled here.
> +	 */
> +	EXPECT_EQ(0, signal_received);
> +
> +	ASSERT_EQ(child, waitpid(child, &status, 0));
> +	if (WIFSIGNALED(status) || !WIFEXITED(status) ||
> +	    WEXITSTATUS(status) != EXIT_SUCCESS)
> +		_metadata->exit_code = KSFT_FAIL;
> +}
> +
> +static void *thread_setown_scoped(void *arg)
> +{
> +	const int fd = *(int *)arg;
> +	int ruleset_fd;
> +	const struct landlock_ruleset_attr ruleset_attr = {
> +		.scoped = LANDLOCK_SCOPE_SIGNAL,
> +	};
> +
> +	/* Sandboxes only this non-leader thread (no thread syncing). */
> +	ruleset_fd =
> +		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
> +	if (ruleset_fd < 0)
> +		return (void *)THREAD_ERROR;
> +	if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) ||
> +	    landlock_restrict_self(ruleset_fd, 0)) {
> +		close(ruleset_fd);
> +		return (void *)THREAD_ERROR;
> +	}
> +	close(ruleset_fd);
> +
> +	/* Makes this process group own the SIGIO source. */
> +	if (fcntl(fd, F_SETSIG, SIGURG) || fcntl(fd, F_SETOWN, -getpgrp()) ||
> +	    fcntl(fd, F_SETFL, O_ASYNC))
> +		return (void *)THREAD_ERROR;
> +
> +	return (void *)THREAD_SUCCESS;
> +}
> +
> +/*
> + * Checks that the SIGIO fan-out is still delivered to the file owner's own
> + * process when fcntl(F_SETOWN, -pgrp) was issued from a sandboxed non-leader
> + * thread.
> + *
> + * The Landlock domain is recorded for a process-group owner (so out-of-domain
> + * members stay blocked, see sigio_to_pgid_members), but the kernel signals a
> + * process group through its members' thread-group leaders.  Here the leader is
> + * not sandboxed and thus has a different domain than the registering thread, so
> + * the registration-time check cannot tell that it belongs to the owner's own
> + * process.  hook_file_send_sigiotask() must recognize it through the recorded
> + * thread group and allow the delivery, matching the same-process guarantee of
> + * commit 18eb75f3af40.  Without that exemption the leader is wrongly denied and
> + * never signaled.
> + */
> +TEST(sigio_to_pgid_self)
> +{
> +	int trigger[2];
> +	pthread_t thread;
> +	enum thread_return ret = THREAD_INVALID;
> +	int i;
> +
> +	drop_caps(_metadata);
> +
> +	/* Bounds the SIGIO fan-out to this process. */
> +	ASSERT_EQ(0, setpgid(0, 0));
> +
> +	/* The non-sandboxed thread-group leader is the SIGIO target. */
> +	ASSERT_EQ(0, setup_signal_handler(SIGURG));
> +	signal_received = 0;
> +
> +	ASSERT_EQ(0, pipe2(trigger, O_CLOEXEC));
> +
> +	/*
> +	 * Registers the process-group fowner from a sibling thread that
> +	 * sandboxes only itself, so its domain differs from the leader's.
> +	 */
> +	ASSERT_EQ(0, pthread_create(&thread, NULL, thread_setown_scoped,
> +				    &trigger[0]));
> +	ASSERT_EQ(0, pthread_join(thread, (void **)&ret));
> +	ASSERT_EQ(THREAD_SUCCESS, ret);
> +
> +	/* Fans SIGURG out to the process group. */
> +	ASSERT_EQ(1, write(trigger[1], ".", 1));
> +
> +	for (i = 0; i < 1000 && !signal_received; i++)
> +		usleep(1000);
> +
> +	/*
> +	 * Same-process delivery must always be allowed, even though the owner
> +	 * was registered from a sandboxed sibling thread.
> +	 */
> +	EXPECT_EQ(1, signal_received);
> +
> +	EXPECT_EQ(0, close(trigger[0]));
> +	EXPECT_EQ(0, close(trigger[1]));
> +}
> +
>  TEST_HARNESS_MAIN
> -- 
> 2.43.0
> 
> 

Reviewed-by: Günther Noack <gnoack3000@gmail.com>

      reply	other threads:[~2026-06-05 11:50 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-04 23:16 [PATCH v5 0/2] landlock: fix SCOPE_SIGNAL bypass on the SIGIO/fowner path Bryam Vargas
2026-06-04 23:16 ` [PATCH v5 1/2] landlock: fix LANDLOCK_SCOPE_SIGNAL bypass on the SIGIO path Bryam Vargas
2026-06-05 11:11   ` Günther Noack
2026-06-04 23:17 ` [PATCH v5 2/2] selftests/landlock: test SCOPE_SIGNAL on the SIGIO/fowner pgid path Bryam Vargas
2026-06-05 11:50   ` Günther Noack [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260605.b1f90e8b16bd@gnoack.org \
    --to=gnoack3000@gmail.com \
    --cc=brauner@kernel.org \
    --cc=gnoack@google.com \
    --cc=hexlabsecurity@proton.me \
    --cc=jmorris@namei.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-security-module@vger.kernel.org \
    --cc=mic@digikod.net \
    --cc=paul@paul-moore.com \
    --cc=serge@hallyn.com \
    --cc=stable@vger.kernel.org \
    --cc=utilityemal77@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox