From nobody Mon Jun 8 05:25:48 2026 Received: from mail-244122.protonmail.ch (mail-244122.protonmail.ch [109.224.244.122]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id A40343793AB for ; Wed, 3 Jun 2026 01:38:19 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=109.224.244.122 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780450703; cv=none; b=e0sNhpyUuEr5xh3EDVHyzd31vnRwf9gd8RFNAGcwqqIt5ewSBp61tZAlzdCJFkdsqXXN/V0kvtVpk6ivPOlMqTpB7FVLRLI906RIftMx1i+HjNvoDuMZp0lPq+sWgKlE8EmaTrqDAucbH+msk1KT8BIytPZZf74dekEJU5pa0xo= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780450703; c=relaxed/simple; bh=7dpTDpxwEOIzk22YgLvrqs67Vg7Je/wtKwL+vcE03QM=; h=Date:To:From:Cc:Subject:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=Lf64OJCgZA9PBd4oa1duk4Qk2JgjfqK5XkQ4W+RfiI/CvscyhdFVc4alnDqcIkwFE1RRzA4GGU1bCJXzzx270zrMtKxSLiC/+DKKIutDB4XdxcNIS314OU1w81JT8La8TR1lK454ADkQJsldE0XF7xEgEgKbmJCt7/sKA1PGUeQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=proton.me; spf=pass smtp.mailfrom=proton.me; dkim=pass (2048-bit key) header.d=proton.me header.i=@proton.me header.b=lWWRoizP; arc=none smtp.client-ip=109.224.244.122 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=proton.me Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=proton.me Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=proton.me header.i=@proton.me header.b="lWWRoizP" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=proton.me; s=protonmail; t=1780450698; x=1780709898; bh=Xoqh0gHhlYUbvmyM3ulY5nL3TPXotPEdpVBETVb6EVA=; h=Date:To:From:Cc:Subject:Message-ID:In-Reply-To:References: Feedback-ID:From:To:Cc:Date:Subject:Reply-To:Feedback-ID: Message-ID:BIMI-Selector; b=lWWRoizPdrttMmBQQyvx1vXFKvBunBnRxLjSwoT47KOhwpUjbmJNA1dNaXqsG5Vch yxhuFZJ8TAQab/8iPeu0EuOZLh4dAz/Pvj4agljjz/4MwmibNrbQGqhbWr+oraHJ93 bEFtWfnQ6AdUvajuwiLkGNaaCZ3NMmGhg3uAtsyki6TZKuxACZkBdqUzJ6hYyxJnQa qqN5E+tfzdrdsnZa+UEXpa2B5aD64qzuOyrS8a9iD6izZgJitJf90xC4vcj8loe7tZ 5sWqS3ILNWDFG97ynzcgV2GbNKzbYxsAYJdemGxlGMPV4Y/5gH/he8kUK/J/Ui9sad M8oWm2Bw1lcyg== Date: Tue, 02 Jun 2026 17:27:56 +0000 To: =?utf-8?Q?Micka=C3=ABl_Sala=C3=BCn?= , =?utf-8?Q?G=C3=BCnther_Noack?= From: Bryam Vargas Cc: Justin Suess , Christian Brauner , Paul Moore , James Morris , "Serge E . Hallyn" , linux-security-module@vger.kernel.org, stable@vger.kernel.org, linux-kernel@vger.kernel.org Subject: [PATCH v4 1/2] landlock: fix LANDLOCK_SCOPE_SIGNAL bypass on the SIGIO path Message-ID: <20260602172741.18760-2-hexlabsecurity@proton.me> In-Reply-To: <20260602172741.18760-1-hexlabsecurity@proton.me> References: <7rvmLIHR1Zh8RDF1IY1-SYRHzErgw9gPHq0k98RLYVsmHqAejjxcuJi8V3QaSbW-SnNvY5tfM2Xn_S1dEajKV_f7iyitoPwJgOSTZQ0nytc=@proton.me> <20260531.irah0eiM3Chi@digikod.net> <20260602172741.18760-1-hexlabsecurity@proton.me> Feedback-ID: 199661219:user:proton X-Pm-Message-ID: 942fde585501fdf26f189db2f771231337837627 Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" LANDLOCK_SCOPE_SIGNAL must prevent a sandboxed process from signaling processes outside its Landlock domain. It can be bypassed through the asynchronous SIGIO delivery path. A sandboxed process that owns any file or socket can arm it with fcntl(F_SETOWN, fd, -pgid), fcntl(F_SETSIG, fd, SIGKILL) and O_ASYNC, so that an I/O event makes the kernel deliver the chosen signal to the whole process group. As the head of its own process group -- the default right after fork() -- that group also holds the non-sandboxed process that launched it, e.g. a supervisor or a security monitor. The sandbox can thus kill or repeatedly signal exactly the processes SCOPE_SIGNAL is meant to protect from it. The scope is enforced in hook_file_send_sigiotask() against the Landlock domain recorded at F_SETOWN time, not the live domain of the sender. control_current_fowner() decides whether to record that domain and skips recording it when the fowner target is in the caller's thread group -- safe only when the target is a single process sharing the caller's credentials (PIDTYPE_PID, PIDTYPE_TGID). For a process group (PIDTYPE_PGID) the target resolves to the caller itself when it is the group head, recording is skipped, and hook_file_send_sigiotask() then lets the signal fan out to the whole group unchecked. Skip the recording only for the single-process target types, so the scope is enforced against every group member at delivery time. The direct kill() path (hook_task_kill) already evaluates the live domain and is unaffected. Fixes: 18eb75f3af40 ("landlock: Always allow signals between threads of the= same process") Cc: stable@vger.kernel.org Tested-by: Justin Suess Signed-off-by: Bryam Vargas --- security/landlock/fs.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/security/landlock/fs.c b/security/landlock/fs.c index c1ecfe239032..2ebad70a956d 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -1909,6 +1909,15 @@ static bool control_current_fowner(struct fown_struc= t *const fown) if (!p) return true; =20 + /* + * A process-group fowner fans the signal out to every member at + * delivery time, so record the domain for any non single-process + * target -- even when it resolves to current as the group head -- + * and let hook_file_send_sigiotask() check the live scope. + */ + if (fown->pid_type !=3D PIDTYPE_PID && fown->pid_type !=3D PIDTYPE_TGID) + return true; + return !same_thread_group(p, current); } =20 --=20 2.43.0 From nobody Mon Jun 8 05:25:48 2026 Received: from mail-24418.protonmail.ch (mail-24418.protonmail.ch [109.224.244.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 7506D37C937 for ; Wed, 3 Jun 2026 01:38:28 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=109.224.244.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780450711; cv=none; b=XulWzt/euAL46v5ziVBQp+am2Xsm8F3hyV+ksPWH1DggtOf+vZArh08X2RKF941MrUp2RGkyQ8qKqMnVhScAn/2SzLAcekmQ57iFmwJYH/49wZcnR3REn8SyIVLA/QANQl/v82ETpDi5dv6yU7O1+ANzDazPDny+NZDNyNhacJg= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780450711; c=relaxed/simple; bh=QxrzYeHZ9k2TdtXLJ7NosY0R953zUVso6EmSGeoENEE=; h=Date:To:From:Cc:Subject:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=u2oi+zfoJGhmg1xM1cYXoY07gejJE4FXcQnNdu+2LSgxlkn3gDrfdKB4atMAn04jl3eHJD4XmSIrmFvhwLheEPuqk0kNveo/gOFaVY1vocMHEd30jxL3IVn9kStEraPksKbn7AtunvtF8OEFXzcqdUlLrwA8TtGXzoBx+sAJX4o= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=proton.me; spf=pass smtp.mailfrom=proton.me; dkim=pass (2048-bit key) header.d=proton.me header.i=@proton.me header.b=dGlHD/3/; arc=none smtp.client-ip=109.224.244.18 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=proton.me Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=proton.me Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=proton.me header.i=@proton.me header.b="dGlHD/3/" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=proton.me; s=protonmail; t=1780450706; x=1780709906; bh=cRGdVuL25/+8RzBGzRV7c+495EWiR/Aa1Z4J451vdts=; h=Date:To:From:Cc:Subject:Message-ID:In-Reply-To:References: Feedback-ID:From:To:Cc:Date:Subject:Reply-To:Feedback-ID: Message-ID:BIMI-Selector; b=dGlHD/3/ghgt39AJb2Ixbo7JOiaPweeFmEAwkwquDb4jCwLzWaPvckLeR+H2U+0Uy mNHflpTf5Pg5HvN0AdP1xGz2BmapYjtAFlrHZAJPE+86Ly8O2ERQlbd//sj3RmSDeD LAEAWILtnYnjJMcRq0r2vcWm6SuH0e6YNbJSBchcJZB3bxoWoAp13ACQPm08jNMqzL O51seE2IvBH5pKyH2OUxEe3jnF7f56TOwWdj8WPDRENShwEv0verZkY2OUF1Yi1izT dqJOY8ZH6rtAn8nw8hYwPO01wjZpgSDKZJWC7OfRiAVERDGSCzCqpktZJseEK+iFKy BsOblr/Rbzh7A== Date: Tue, 02 Jun 2026 17:28:05 +0000 To: =?utf-8?Q?Micka=C3=ABl_Sala=C3=BCn?= , =?utf-8?Q?G=C3=BCnther_Noack?= From: Bryam Vargas Cc: Justin Suess , Christian Brauner , Paul Moore , James Morris , "Serge E . Hallyn" , linux-security-module@vger.kernel.org, stable@vger.kernel.org, linux-kernel@vger.kernel.org Subject: [PATCH v4 2/2] selftests/landlock: test SCOPE_SIGNAL on the SIGIO/fowner pgid path Message-ID: <20260602172741.18760-3-hexlabsecurity@proton.me> In-Reply-To: <20260602172741.18760-1-hexlabsecurity@proton.me> References: <7rvmLIHR1Zh8RDF1IY1-SYRHzErgw9gPHq0k98RLYVsmHqAejjxcuJi8V3QaSbW-SnNvY5tfM2Xn_S1dEajKV_f7iyitoPwJgOSTZQ0nytc=@proton.me> <20260531.irah0eiM3Chi@digikod.net> <20260602172741.18760-1-hexlabsecurity@proton.me> Feedback-ID: 199661219:user:proton X-Pm-Message-ID: 335be816ca89f676268cd269abca1c33a7fd1f39 Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add a regression test for the LANDLOCK_SCOPE_SIGNAL bypass on the asynchronous SIGIO delivery path. A sandboxed process that owns a file via fcntl(F_SETOWN, -pgrp) while sitting at the head of its process group's PID hlist (the default position after fork()) used to have its Landlock domain recording skipped, letting the SIGIO fan-out reach non-sandboxed members of the process group. The test creates a dedicated process group, sandboxes the (hlist-head) child with LANDLOCK_SCOPE_SIGNAL, arms F_SETSIG(SIGURG) / F_SETOWN(-pgrp) / O_ASYNC on a pipe and triggers the fan-out. The in-domain child must receive the signal (proving the trigger fired); the non-sandboxed parent, which is outside the child's domain, must not. Without the fix the parent is signaled and the test fails. Signed-off-by: Bryam Vargas --- .../selftests/landlock/scoped_signal_test.c | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/tools/testing/selftests/landlock/scoped_signal_test.c b/tools/= testing/selftests/landlock/scoped_signal_test.c index d8bf33417619..62d86a115775 100644 --- a/tools/testing/selftests/landlock/scoped_signal_test.c +++ b/tools/testing/selftests/landlock/scoped_signal_test.c @@ -559,4 +559,101 @@ TEST_F(fown, sigurg_socket) _metadata->exit_code =3D KSFT_FAIL; } =20 +/* + * 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 skip= ped + * 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 =3D 0; + + ASSERT_EQ(0, pipe2(trigger, O_CLOEXEC)); + ASSERT_EQ(0, pipe2(sync_child, O_CLOEXEC)); + + child =3D fork(); + ASSERT_LE(0, child); + if (child =3D=3D 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 =3D 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 =3D 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 =3D 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) !=3D EXIT_SUCCESS) + _metadata->exit_code =3D KSFT_FAIL; +} + TEST_HARNESS_MAIN --=20 2.43.0