The Linux Kernel Mailing List
 help / color / mirror / Atom feed
From: "Günther Noack" <gnoack3000@gmail.com>
To: Justin Suess <utilityemal77@gmail.com>
Cc: mic@digikod.net, linux-kernel@vger.kernel.org,
	linux-security-module@vger.kernel.org
Subject: Re: [PATCH v2 4/6] selftests/landlock: Test LANDLOCK_SCOPE_SYSV_MSG_QUEUE
Date: Sat, 22 Aug 2026 11:44:08 +0200	[thread overview]
Message-ID: <20260822.6e90a5c8196c@gnoack.org> (raw)
In-Reply-To: <20260727230833.138165-5-utilityemal77@gmail.com>

Hello!

On Mon, Jul 27, 2026 at 07:08:31PM -0400, Justin Suess wrote:
> Add selftests for SysV message queue scoped right.
> 
> Use the existing scoped domain harness for msgget, and another fixture
> for testing msgsnd, msgrcv and msgctl.
> 
> Pass the msqid around for coverage of non-msgget syscalls, since calling
> msgget while already restricted would fail and prevent testing the
> operation under test.
> 
> Denials are checked against -EACCES rather than -EPERM: msgget,
> msgsnd, msgrcv and msgctl(IPC_STAT) all reach the Landlock scope
> check via ipcperms(), whose callers map every non-zero return into
> -EACCES before propagating it to user space.
> 
> Track the created msqid in the fixture and remove it from
> FIXTURE_TEARDOWN_PARENT() so that queues are reclaimed even when a
> failed assertion aborts a test, and so that the removal is never
> subject to the scoping under test.
> 
> Also add CONFIG_SYSVIPC to the selftest config fragment since the new
> test requires SysV IPC support.
> 
> Signed-off-by: Justin Suess <utilityemal77@gmail.com>
> ---
>  tools/testing/selftests/landlock/config       |   1 +
>  .../landlock/scoped_sysv_msg_queue_test.c     | 265 ++++++++++++++++++
>  2 files changed, 266 insertions(+)
>  create mode 100644 tools/testing/selftests/landlock/scoped_sysv_msg_queue_test.c
> 
> diff --git a/tools/testing/selftests/landlock/config b/tools/testing/selftests/landlock/config
> index 8fe9b461b1fd..8acb03464df4 100644
> --- a/tools/testing/selftests/landlock/config
> +++ b/tools/testing/selftests/landlock/config
> @@ -15,5 +15,6 @@ CONFIG_SECURITY=y
>  CONFIG_SECURITY_LANDLOCK=y
>  CONFIG_SHMEM=y
>  CONFIG_SYSFS=y
> +CONFIG_SYSVIPC=y
>  CONFIG_TMPFS=y
>  CONFIG_TMPFS_XATTR=y
> diff --git a/tools/testing/selftests/landlock/scoped_sysv_msg_queue_test.c b/tools/testing/selftests/landlock/scoped_sysv_msg_queue_test.c
> new file mode 100644
> index 000000000000..91a560c957e6
> --- /dev/null
> +++ b/tools/testing/selftests/landlock/scoped_sysv_msg_queue_test.c
> @@ -0,0 +1,265 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Landlock tests - SysV Message Queue Scoping
> + *

Extraneous blank comment line here; did you mean to add a Copyright
line here?

> + */
> +
> +#define _GNU_SOURCE
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <linux/landlock.h>
> +#include <sys/ipc.h>
> +#include <sys/msg.h>
> +#include <sys/types.h>
> +#include <sys/wait.h>
> +#include <unistd.h>
> +
> +#include "common.h"
> +#include "scoped_common.h"
> +
> +/*
> + * Removes the message queue identified by @msqid, ignoring any error since
> + * the caller might no longer have permission to operate on it (for example,
> + * after entering a scoped domain).
> + */
> +static void cleanup_msg_queue(int msqid)
> +{
> +	if (msqid >= 0)
> +		msgctl(msqid, IPC_RMID, NULL);
> +}
> +
> +FIXTURE(scoped_domains)
> +{
> +	int msqid;
> +};
> +
> +#include "scoped_base_variants.h"
> +
> +FIXTURE_SETUP(scoped_domains)
> +{
> +	self->msqid = -1;
> +	drop_caps(_metadata);
> +}
> +
> +/*
> + * The queue is removed by the (never sandboxed) test harness parent, which
> + * also runs when an assertion aborts the test after the queue got created.
> + */
> +FIXTURE_TEARDOWN_PARENT(scoped_domains)
> +{
> +	cleanup_msg_queue(self->msqid);
> +}
> +
> +/*
> + * Parent creates a SysV message queue, then the child tries to associate
> + * with it via msgget(2).  When the child is in a domain that scopes message
> + * queues and the parent is not in that same scope, the association must be
> + * denied with -EACCES (msgget runs the scope check via ipcperms(), which
> + * masks every denial as -EACCES).
> + */
> +TEST_F(scoped_domains, check_access_msg_queue)
> +{
> +	pid_t child;
> +	int status;
> +	int pipe_parent[2], pipe_child[2];
> +	char buf;
> +	key_t key;
> +	bool can_associate;
> +
> +	/*
> +	 * The child can associate with the parent's queue unless the child
> +	 * is in a scoped domain that does not include the parent (i.e. the
> +	 * parent is outside the child's domain).
> +	 */
> +	can_associate = !variant->domain_child;
> +
> +	/*
> +	 * Picks a per-test key derived from PID to avoid collisions.  Stale
> +	 * queues from a previous run are unlikely but handled by removing
> +	 * any matching entry before applying any scope.
> +	 */
> +	key = (key_t)(getpid() & 0x7fffffff);

Is that supported? :)

Michael Kerrisk's Linux Programming Interface book only covered ftok()
and IPC_PRIVATE.  In my understanding, if you pass IPC_PRIVATE to
msgget(), you are guaranteed to get a new queue?  You are using that
in the other test below as well.  Isn't that what you want?


> +	cleanup_msg_queue(msgget(key, 0));
> +
> +	if (variant->domain_both)
> +		create_scoped_domain(_metadata, LANDLOCK_SCOPE_SYSV_MSG_QUEUE);
> +
> +	ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
> +	ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
> +
> +	child = fork();
> +	ASSERT_LE(0, child);
> +	if (child == 0) {
> +		int ret;
> +
> +		EXPECT_EQ(0, close(pipe_child[0]));
> +		EXPECT_EQ(0, close(pipe_parent[1]));
> +
> +		if (variant->domain_child)
> +			create_scoped_domain(_metadata,
> +					     LANDLOCK_SCOPE_SYSV_MSG_QUEUE);
> +
> +		/* Signals readiness to the parent. */
> +		ASSERT_EQ(1, write(pipe_child[1], ".", 1));
> +		EXPECT_EQ(0, close(pipe_child[1]));
> +
> +		/* Waits for the parent to have created the queue. */
> +		ASSERT_EQ(1, read(pipe_parent[0], &buf, 1));
> +		EXPECT_EQ(0, close(pipe_parent[0]));
> +
> +		ret = msgget(key, 0);
> +		if (can_associate) {
> +			ASSERT_LE(0, ret);
> +		} else {
> +			ASSERT_EQ(-1, ret);
> +			/*
> +			 * msgget uses ipcperms(), which masks every LSM
> +			 * denial as -EACCES regardless of the value the
> +			 * LSM hook returns.
> +			 */
> +			ASSERT_EQ(EACCES, errno);
> +		}

Testing nit: The checks on the msgget() results should probably be an
EXPECT_*() variant. ASSERT_*() should only be used when continuing to
run the test doesn't otherwise make sense.

Compare https://google.github.io/googletest/reference/assertions.html:

    The majority of the macros listed below come as a pair with an
    EXPECT_ variant and an ASSERT_ variant. Upon failure, EXPECT_
    macros generate nonfatal failures and allow the current function
    to continue running, while ASSERT_ macros generate fatal failures
    and abort the current function.


> +
> +		_exit(_metadata->exit_code);
> +		return;
> +	}
> +	EXPECT_EQ(0, close(pipe_child[1]));
> +	EXPECT_EQ(0, close(pipe_parent[0]));
> +
> +	if (variant->domain_parent)
> +		create_scoped_domain(_metadata, LANDLOCK_SCOPE_SYSV_MSG_QUEUE);
> +
> +	/* Waits for the child to be ready. */
> +	ASSERT_EQ(1, read(pipe_child[0], &buf, 1));
> +	EXPECT_EQ(0, close(pipe_child[0]));
> +
> +	self->msqid = msgget(key, IPC_CREAT | IPC_EXCL | 0600);
> +	ASSERT_LE(0, self->msqid);
> +
> +	/* Releases the child. */
> +	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
> +	EXPECT_EQ(0, close(pipe_parent[1]));
> +
> +	ASSERT_EQ(child, waitpid(child, &status, 0));
> +
> +	if (WIFSIGNALED(status) || !WIFEXITED(status) ||
> +	    WEXITSTATUS(status) != EXIT_SUCCESS)
> +		_metadata->exit_code = KSFT_FAIL;
> +}
> +
> +/*
> + * The msg_queue_associate hook (exercised by msgget(2)) is covered by the
> + * scoped_domains fixture above.  The remaining hooks all funnel through the
> + * same scope check, so it suffices to verify that each operation is denied
> + * when the child is scoped relative to the queue's creator.
> + *
> + * To attribute a denial to the operation under test (and not to a preceding
> + * msgget(2) call), the parent creates the queue and the child inherits the
> + * msqid across fork(2), bypassing msg_queue_associate.
> + */
> +enum msg_op {
> +	MSG_OP_SND,
> +	MSG_OP_RCV,
> +	MSG_OP_CTL,
> +};
> +
> +FIXTURE(scoping_msg_ops)
> +{
> +	int msqid;
> +};
> +
> +FIXTURE_VARIANT(scoping_msg_ops)
> +{
> +	enum msg_op op;
> +};
> +
> +/* clang-format off */
> +FIXTURE_VARIANT_ADD(scoping_msg_ops, msgsnd) {
> +	/* clang-format on */
> +	.op = MSG_OP_SND,
> +};
> +
> +/* clang-format off */
> +FIXTURE_VARIANT_ADD(scoping_msg_ops, msgrcv) {
> +	/* clang-format on */
> +	.op = MSG_OP_RCV,
> +};
> +
> +/* clang-format off */
> +FIXTURE_VARIANT_ADD(scoping_msg_ops, msgctl) {
> +	/* clang-format on */
> +	.op = MSG_OP_CTL,
> +};
> +
> +FIXTURE_SETUP(scoping_msg_ops)
> +{
> +	self->msqid = -1;
> +	drop_caps(_metadata);
> +}
> +
> +/* See the scoped_domains teardown comment. */
> +FIXTURE_TEARDOWN_PARENT(scoping_msg_ops)
> +{
> +	cleanup_msg_queue(self->msqid);
> +}
> +
> +TEST_F(scoping_msg_ops, deny_op)
> +{
> +	struct msgbuf {
> +		long mtype;
> +		char mtext[1];
> +	} msg = { .mtype = 1 };
> +	struct msqid_ds ds;
> +	pid_t child;
> +	int status;
> +	int ret = 0;
> +
> +	/*
> +	 * The child inherits the msqid across fork(2), so no key is needed:
> +	 * IPC_PRIVATE always creates a new queue and cannot collide with
> +	 * queues left over by other processes.
> +	 */
> +	self->msqid = msgget(IPC_PRIVATE, 0600);
> +	ASSERT_LE(0, self->msqid);
> +
> +	/* Preloads a message so msgrcv(2) would otherwise succeed. */
> +	ASSERT_EQ(0, msgsnd(self->msqid, &msg, sizeof(msg.mtext), 0));
> +
> +	child = fork();

I do not understand why this test needs to fork() at all, tbh.
If you were to

* create the queue with msgget(),
* add a message with msgsnd(),
* then enter the Landlock domain (still in the same process),
* and then check for the operation failure

Wouldn't that also fail?  After all, the message queue was created at
a point in time when the process was still unrestricted, so we should
be comparing to the Landlock state at that earlier point in time, no?

> +	ASSERT_LE(0, child);
> +	if (child == 0) {
> +		create_scoped_domain(_metadata, LANDLOCK_SCOPE_SYSV_MSG_QUEUE);
> +
> +		switch (variant->op) {
> +		case MSG_OP_SND:
> +			ret = msgsnd(self->msqid, &msg, sizeof(msg.mtext), 0);
> +			break;
> +		case MSG_OP_RCV:
> +			ret = msgrcv(self->msqid, &msg, sizeof(msg.mtext), 0,
> +				     IPC_NOWAIT);
> +			break;
> +		case MSG_OP_CTL:
> +			ret = msgctl(self->msqid, IPC_STAT, &ds);
> +			break;
> +		}
> +		ASSERT_EQ(-1, ret);

If these operations are all supposed to fail (and presumably be
no-ops), is it necessary to still use a fixture with multiple cases
for that?  (The alternative would be to make this a regular TEST()
without fixtures and do the three failing operations one after
another. It would save you a lot of macro boilerplate and the special
enum, and would make the test more direct IMHO.)

> +		/*
> +		 * msgsnd, msgrcv and msgctl(IPC_STAT) all reach the
> +		 * Landlock scope check via ipcperms(), whose callers map
> +		 * any non-zero return into -EACCES before propagating it
> +		 * to user space.
> +		 */
> +		ASSERT_EQ(EACCES, errno);
> +
> +		_exit(_metadata->exit_code);
> +		return;
> +	}
> +
> +	ASSERT_EQ(child, waitpid(child, &status, 0));
> +
> +	if (WIFSIGNALED(status) || !WIFEXITED(status) ||
> +	    WEXITSTATUS(status) != EXIT_SUCCESS)
> +		_metadata->exit_code = KSFT_FAIL;
> +}
> +
> +TEST_HARNESS_MAIN
> -- 
> 2.54.0
> 

–Günther

  reply	other threads:[~2026-08-22  9:44 UTC|newest]

Thread overview: 18+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-27 23:08 [PATCH v2 0/6] landlock: Add scoped access bit for SysV message queues Justin Suess
2026-07-27 23:08 ` [PATCH v2 1/6] landlock: Add kern_ipc_perm credential blob structs Justin Suess
2026-07-27 23:08 ` [PATCH v2 2/6] landlock: Add LANDLOCK_SCOPE_SYSV_MSG_QUEUE Justin Suess
2026-07-27 23:08 ` [PATCH v2 3/6] landlock: Bump ABI for LANDLOCK_SCOPE_SYSV_MSG_QUEUE Justin Suess
2026-08-21 12:38   ` Günther Noack
2026-08-21 13:15     ` Justin Suess
2026-08-21 14:31       ` Justin Suess
2026-07-27 23:08 ` [PATCH v2 4/6] selftests/landlock: Test LANDLOCK_SCOPE_SYSV_MSG_QUEUE Justin Suess
2026-08-22  9:44   ` Günther Noack [this message]
2026-07-27 23:08 ` [PATCH v2 5/6] samples/landlock: Support LANDLOCK_SCOPE_SYSV_MSG_QUEUE in sandboxer Justin Suess
2026-07-27 23:08 ` [PATCH v2 6/6] landlock: Document LANDLOCK_SCOPE_SYSV_MSG_QUEUE Justin Suess
2026-08-22 17:26 ` [PATCH v2 0/6] landlock: Add scoped access bit for SysV message queues Günther Noack
2026-08-22 21:13   ` Günther Noack
2026-08-22 21:51     ` Justin Suess
2026-08-23 22:21       ` Günther Noack
2026-08-24  6:36       ` Günther Noack
2026-08-24 12:47         ` Justin Suess
2026-08-24 16:03           ` Günther Noack

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=20260822.6e90a5c8196c@gnoack.org \
    --to=gnoack3000@gmail.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-security-module@vger.kernel.org \
    --cc=mic@digikod.net \
    --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