[PATCH v2 5/6] selftests/landlock: Repurpose scoped_abstract_unix_test.c for pathname sockets too.
Mickaël Salaün
mic at digikod.net
Thu Jan 29 21:28:10 UTC 2026
Commit messages should fit in 72 columns. The subject can be a bit more
but we should avoid that, and it should not end with a dot.
On Tue, Dec 30, 2025 at 05:20:23PM +0000, Tingmao Wang wrote:
> Since there is very little difference between abstract and pathname
> sockets in terms of testing of the scoped access checks (the only
> difference is in which scope bit control which form of socket), it makes
> sense to reuse the existing test for both type of sockets. Therefore, we
> rename scoped_abstract_unix_test.c to scoped_unix_test.c and extend the
> scoped_domains test to test pathname (i.e. non-abstract) sockets too.
>
> Since we can't change the variant data of scoped_domains (as it is defined
> in the shared .h file), we do this by extracting the actual test code into
> a function, and call it from different test cases.
>
> Also extend scoped_audit (this time we can use variants) to test both
> abstract and pathname sockets. For pathname sockets, audit_log_lsm_data
> will produce path="..." (or hex if path contains control characters) with
> absolute paths from the dentry, so we need to construct the escaped regex
> for the real path like in fs_test.
>
> Signed-off-by: Tingmao Wang <m at maowtm.org>
> ---
> ...bstract_unix_test.c => scoped_unix_test.c} | 256 ++++++++++++++----
> 1 file changed, 206 insertions(+), 50 deletions(-)
> rename tools/testing/selftests/landlock/{scoped_abstract_unix_test.c => scoped_unix_test.c} (81%)
>
> diff --git a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c b/tools/testing/selftests/landlock/scoped_unix_test.c
> similarity index 81%
> rename from tools/testing/selftests/landlock/scoped_abstract_unix_test.c
> rename to tools/testing/selftests/landlock/scoped_unix_test.c
> index 4a790e2d387d..669418c97509 100644
> --- a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c
> +++ b/tools/testing/selftests/landlock/scoped_unix_test.c
> @@ -1,6 +1,7 @@
> // SPDX-License-Identifier: GPL-2.0
> /*
> - * Landlock tests - Abstract UNIX socket
> + * Landlock tests - Scoped access checks for UNIX socket (abstract and
> + * pathname)
> *
> * Copyright © 2024 Tahera Fahimi <fahimitahera at gmail.com>
> */
> @@ -19,6 +20,7 @@
> #include <sys/un.h>
> #include <sys/wait.h>
> #include <unistd.h>
> +#include <stdlib.h>
>
> #include "audit.h"
> #include "common.h"
> @@ -47,7 +49,8 @@ static void create_fs_domain(struct __test_metadata *const _metadata)
>
> FIXTURE(scoped_domains)
> {
> - struct service_fixture stream_address, dgram_address;
> + struct service_fixture stream_address_abstract, dgram_address_abstract,
> + stream_address_pathname, dgram_address_pathname;
> };
>
> #include "scoped_base_variants.h"
> @@ -56,27 +59,62 @@ FIXTURE_SETUP(scoped_domains)
> {
> drop_caps(_metadata);
>
> - memset(&self->stream_address, 0, sizeof(self->stream_address));
> - memset(&self->dgram_address, 0, sizeof(self->dgram_address));
> - set_unix_address(&self->stream_address, 0, true);
> - set_unix_address(&self->dgram_address, 1, true);
> + ASSERT_EQ(0, mkdir(PATHNAME_UNIX_SOCK_DIR, 0700));
> +
> + memset(&self->stream_address_abstract, 0,
> + sizeof(self->stream_address_abstract));
> + memset(&self->dgram_address_abstract, 0,
> + sizeof(self->dgram_address_abstract));
> + memset(&self->stream_address_pathname, 0,
> + sizeof(self->stream_address_pathname));
> + memset(&self->dgram_address_pathname, 0,
> + sizeof(self->dgram_address_pathname));
> + set_unix_address(&self->stream_address_abstract, 0, true);
> + set_unix_address(&self->dgram_address_abstract, 1, true);
> + set_unix_address(&self->stream_address_pathname, 0, false);
> + set_unix_address(&self->dgram_address_pathname, 1, false);
> +}
> +
> +/* Remove @path if it exists */
> +int remove_path(const char *path)
> +{
> + if (unlink(path) == -1) {
> + if (errno != ENOENT)
> + return -errno;
> + }
> + return 0;
> }
>
> FIXTURE_TEARDOWN(scoped_domains)
> {
> + EXPECT_EQ(0, remove_path(self->stream_address_pathname.unix_addr.sun_path));
> + EXPECT_EQ(0, remove_path(self->dgram_address_pathname.unix_addr.sun_path));
> + EXPECT_EQ(0, rmdir(PATHNAME_UNIX_SOCK_DIR));
> }
>
> /*
> * Test unix_stream_connect() and unix_may_send() for a child connecting to its
> * parent, when they have scoped domain or no domain.
> */
> -TEST_F(scoped_domains, connect_to_parent)
> +static void test_connect_to_parent(struct __test_metadata *const _metadata,
> + FIXTURE_DATA(scoped_domains) * self,
> + const FIXTURE_VARIANT(scoped_domains) *
> + variant,
> + const bool abstract)
> {
> pid_t child;
> bool can_connect_to_parent;
> int status;
> int pipe_parent[2];
> int stream_server, dgram_server;
> + const __u16 scope = abstract ? LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET :
> + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
> + const struct service_fixture *stream_address =
> + abstract ? &self->stream_address_abstract :
> + &self->stream_address_pathname;
> + const struct service_fixture *dgram_address =
> + abstract ? &self->dgram_address_abstract :
> + &self->dgram_address_pathname;
>
> /*
> * can_connect_to_parent is true if a child process can connect to its
> @@ -87,8 +125,7 @@ TEST_F(scoped_domains, connect_to_parent)
>
> ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
> if (variant->domain_both) {
> - create_scoped_domain(_metadata,
> - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
> if (!__test_passed(_metadata))
> return;
> }
> @@ -102,8 +139,7 @@ TEST_F(scoped_domains, connect_to_parent)
>
> EXPECT_EQ(0, close(pipe_parent[1]));
> if (variant->domain_child)
> - create_scoped_domain(
> - _metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
>
> stream_client = socket(AF_UNIX, SOCK_STREAM, 0);
> ASSERT_LE(0, stream_client);
> @@ -113,8 +149,8 @@ TEST_F(scoped_domains, connect_to_parent)
> /* Waits for the server. */
> ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
>
> - err = connect(stream_client, &self->stream_address.unix_addr,
> - self->stream_address.unix_addr_len);
> + err = connect(stream_client, &stream_address->unix_addr,
> + stream_address->unix_addr_len);
> if (can_connect_to_parent) {
> EXPECT_EQ(0, err);
> } else {
> @@ -123,8 +159,8 @@ TEST_F(scoped_domains, connect_to_parent)
> }
> EXPECT_EQ(0, close(stream_client));
>
> - err = connect(dgram_client, &self->dgram_address.unix_addr,
> - self->dgram_address.unix_addr_len);
> + err = connect(dgram_client, &dgram_address->unix_addr,
> + dgram_address->unix_addr_len);
> if (can_connect_to_parent) {
> EXPECT_EQ(0, err);
> } else {
> @@ -137,17 +173,16 @@ TEST_F(scoped_domains, connect_to_parent)
> }
> EXPECT_EQ(0, close(pipe_parent[0]));
> if (variant->domain_parent)
> - create_scoped_domain(_metadata,
> - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
>
> stream_server = socket(AF_UNIX, SOCK_STREAM, 0);
> ASSERT_LE(0, stream_server);
> dgram_server = socket(AF_UNIX, SOCK_DGRAM, 0);
> ASSERT_LE(0, dgram_server);
> - ASSERT_EQ(0, bind(stream_server, &self->stream_address.unix_addr,
> - self->stream_address.unix_addr_len));
> - ASSERT_EQ(0, bind(dgram_server, &self->dgram_address.unix_addr,
> - self->dgram_address.unix_addr_len));
> + ASSERT_EQ(0, bind(stream_server, &stream_address->unix_addr,
> + stream_address->unix_addr_len));
> + ASSERT_EQ(0, bind(dgram_server, &dgram_address->unix_addr,
> + dgram_address->unix_addr_len));
> ASSERT_EQ(0, listen(stream_server, backlog));
>
> /* Signals to child that the parent is listening. */
> @@ -166,7 +201,11 @@ TEST_F(scoped_domains, connect_to_parent)
> * Test unix_stream_connect() and unix_may_send() for a parent connecting to
> * its child, when they have scoped domain or no domain.
> */
> -TEST_F(scoped_domains, connect_to_child)
> +static void test_connect_to_child(struct __test_metadata *const _metadata,
> + FIXTURE_DATA(scoped_domains) * self,
> + const FIXTURE_VARIANT(scoped_domains) *
> + variant,
> + const bool abstract)
> {
> pid_t child;
> bool can_connect_to_child;
> @@ -174,6 +213,14 @@ TEST_F(scoped_domains, connect_to_child)
> int pipe_child[2], pipe_parent[2];
> char buf;
> int stream_client, dgram_client;
> + const __u16 scope = abstract ? LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET :
> + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
> + const struct service_fixture *stream_address =
> + abstract ? &self->stream_address_abstract :
> + &self->stream_address_pathname;
> + const struct service_fixture *dgram_address =
> + abstract ? &self->dgram_address_abstract :
> + &self->dgram_address_pathname;
>
> /*
> * can_connect_to_child is true if a parent process can connect to its
> @@ -185,8 +232,7 @@ TEST_F(scoped_domains, connect_to_child)
> ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
> ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
> if (variant->domain_both) {
> - create_scoped_domain(_metadata,
> - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
> if (!__test_passed(_metadata))
> return;
> }
> @@ -199,8 +245,7 @@ TEST_F(scoped_domains, connect_to_child)
> EXPECT_EQ(0, close(pipe_parent[1]));
> EXPECT_EQ(0, close(pipe_child[0]));
> if (variant->domain_child)
> - create_scoped_domain(
> - _metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
>
> /* Waits for the parent to be in a domain, if any. */
> ASSERT_EQ(1, read(pipe_parent[0], &buf, 1));
> @@ -209,11 +254,10 @@ TEST_F(scoped_domains, connect_to_child)
> ASSERT_LE(0, stream_server);
> dgram_server = socket(AF_UNIX, SOCK_DGRAM, 0);
> ASSERT_LE(0, dgram_server);
> - ASSERT_EQ(0,
> - bind(stream_server, &self->stream_address.unix_addr,
> - self->stream_address.unix_addr_len));
> - ASSERT_EQ(0, bind(dgram_server, &self->dgram_address.unix_addr,
> - self->dgram_address.unix_addr_len));
> + ASSERT_EQ(0, bind(stream_server, &stream_address->unix_addr,
> + stream_address->unix_addr_len));
> + ASSERT_EQ(0, bind(dgram_server, &dgram_address->unix_addr,
> + dgram_address->unix_addr_len));
> ASSERT_EQ(0, listen(stream_server, backlog));
>
> /* Signals to the parent that child is listening. */
> @@ -230,8 +274,7 @@ TEST_F(scoped_domains, connect_to_child)
> EXPECT_EQ(0, close(pipe_parent[0]));
>
> if (variant->domain_parent)
> - create_scoped_domain(_metadata,
> - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
>
> /* Signals that the parent is in a domain, if any. */
> ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
> @@ -243,11 +286,11 @@ TEST_F(scoped_domains, connect_to_child)
>
> /* Waits for the child to listen */
> ASSERT_EQ(1, read(pipe_child[0], &buf, 1));
> - err_stream = connect(stream_client, &self->stream_address.unix_addr,
> - self->stream_address.unix_addr_len);
> + err_stream = connect(stream_client, &stream_address->unix_addr,
> + stream_address->unix_addr_len);
> errno_stream = errno;
> - err_dgram = connect(dgram_client, &self->dgram_address.unix_addr,
> - self->dgram_address.unix_addr_len);
> + err_dgram = connect(dgram_client, &dgram_address->unix_addr,
> + dgram_address->unix_addr_len);
> errno_dgram = errno;
> if (can_connect_to_child) {
> EXPECT_EQ(0, err_stream);
> @@ -268,19 +311,79 @@ TEST_F(scoped_domains, connect_to_child)
> _metadata->exit_code = KSFT_FAIL;
> }
>
> +/*
> + * Test unix_stream_connect() and unix_may_send() for a child connecting to its
> + * parent, when they have scoped domain or no domain.
> + */
> +TEST_F(scoped_domains, abstract_connect_to_parent)
> +{
> + test_connect_to_parent(_metadata, self, variant, true);
> +}
> +
> +/*
> + * Test unix_stream_connect() and unix_may_send() for a parent connecting to
> + * its child, when they have scoped domain or no domain.
> + */
> +TEST_F(scoped_domains, abstract_connect_to_child)
> +{
> + test_connect_to_child(_metadata, self, variant, true);
> +}
> +
> +/*
> + * Test unix_stream_connect() and unix_may_send() for a child connecting to its
> + * parent with pathname sockets.
> + */
> +TEST_F(scoped_domains, pathname_connect_to_parent)
> +{
> + test_connect_to_parent(_metadata, self, variant, false);
> +}
> +
> +/*
> + * Test unix_stream_connect() and unix_may_send() for a parent connecting to
> + * its child with pathname sockets.
> + */
> +TEST_F(scoped_domains, pathname_connect_to_child)
> +{
> + test_connect_to_child(_metadata, self, variant, false);
> +}
> +
> FIXTURE(scoped_audit)
> {
> - struct service_fixture dgram_address;
> + struct service_fixture dgram_address_abstract, dgram_address_pathname;
> struct audit_filter audit_filter;
> int audit_fd;
> };
>
> +FIXTURE_VARIANT(scoped_audit)
> +{
> + const bool abstract_socket;
> +};
> +
> +// clang-format off
We always use /* */ comments. Ditto for all clang-format markups.
> +FIXTURE_VARIANT_ADD(scoped_audit, abstract_socket)
> +{
> + // clang-format on
> + .abstract_socket = true,
> +};
> +
> +// clang-format off
> +FIXTURE_VARIANT_ADD(scoped_audit, pathname_socket)
> +{
> + // clang-format on
> + .abstract_socket = false,
> +};
> +
> FIXTURE_SETUP(scoped_audit)
> {
> disable_caps(_metadata);
>
> - memset(&self->dgram_address, 0, sizeof(self->dgram_address));
> - set_unix_address(&self->dgram_address, 1, true);
> + ASSERT_EQ(0, mkdir(PATHNAME_UNIX_SOCK_DIR, 0700));
> + memset(&self->dgram_address_abstract, 0,
> + sizeof(self->dgram_address_abstract));
> + memset(&self->dgram_address_pathname, 0,
> + sizeof(self->dgram_address_pathname));
> + set_unix_address(&self->dgram_address_abstract, 1, true);
> + set_unix_address(&self->dgram_address_pathname, 1, false);
>
> set_cap(_metadata, CAP_AUDIT_CONTROL);
> self->audit_fd = audit_init_with_exe_filter(&self->audit_filter);
> @@ -291,6 +394,8 @@ FIXTURE_SETUP(scoped_audit)
> FIXTURE_TEARDOWN_PARENT(scoped_audit)
> {
> EXPECT_EQ(0, audit_cleanup(-1, NULL));
> + EXPECT_EQ(0, remove_path(self->dgram_address_pathname.unix_addr.sun_path));
> + EXPECT_EQ(0, rmdir(PATHNAME_UNIX_SOCK_DIR));
> }
>
> /* python -c 'print(b"\0selftests-landlock-abstract-unix-".hex().upper())' */
> @@ -308,6 +413,12 @@ TEST_F(scoped_audit, connect_to_child)
> char buf;
> int dgram_client;
> struct audit_records records;
> + struct service_fixture *const dgram_address =
> + variant->abstract_socket ? &self->dgram_address_abstract :
> + &self->dgram_address_pathname;
> + size_t log_match_remaining = 500;
const
Why this number? Could you please follow the same logic as in
matches_log_fs_extra()?
> + char log_match[log_match_remaining];
> + char *log_match_cursor = log_match;
>
> /* Makes sure there is no superfluous logged records. */
> EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
> @@ -330,8 +441,8 @@ TEST_F(scoped_audit, connect_to_child)
>
> dgram_server = socket(AF_UNIX, SOCK_DGRAM, 0);
> ASSERT_LE(0, dgram_server);
> - ASSERT_EQ(0, bind(dgram_server, &self->dgram_address.unix_addr,
> - self->dgram_address.unix_addr_len));
> + ASSERT_EQ(0, bind(dgram_server, &dgram_address->unix_addr,
> + dgram_address->unix_addr_len));
>
> /* Signals to the parent that child is listening. */
> ASSERT_EQ(1, write(pipe_child[1], ".", 1));
> @@ -345,7 +456,9 @@ TEST_F(scoped_audit, connect_to_child)
> EXPECT_EQ(0, close(pipe_child[1]));
> EXPECT_EQ(0, close(pipe_parent[0]));
>
> - create_scoped_domain(_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata,
> + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
> + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET);
>
> /* Signals that the parent is in a domain, if any. */
> ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
> @@ -355,19 +468,62 @@ TEST_F(scoped_audit, connect_to_child)
>
> /* Waits for the child to listen */
> ASSERT_EQ(1, read(pipe_child[0], &buf, 1));
> - err_dgram = connect(dgram_client, &self->dgram_address.unix_addr,
> - self->dgram_address.unix_addr_len);
> + err_dgram = connect(dgram_client, &dgram_address->unix_addr,
> + dgram_address->unix_addr_len);
> EXPECT_EQ(-1, err_dgram);
> EXPECT_EQ(EPERM, errno);
>
> - EXPECT_EQ(
> - 0,
> - audit_match_record(
> - self->audit_fd, AUDIT_LANDLOCK_ACCESS,
> + if (variant->abstract_socket) {
> + log_match_cursor = stpncpy(
> + log_match,
> REGEX_LANDLOCK_PREFIX
> " blockers=scope\\.abstract_unix_socket path=" ABSTRACT_SOCKET_PATH_PREFIX
> "[0-9A-F]\\+$",
> - NULL));
> + log_match_remaining);
> + log_match_remaining =
> + sizeof(log_match) - (log_match_cursor - log_match);
> + ASSERT_NE(0, log_match_remaining);
> + } else {
> + /*
> + * It is assumed that absolute_path does not contain control
> + * characters nor spaces, see audit_string_contains_control().
> + */
> + char *absolute_path =
const char *absolute_path
> + realpath(dgram_address->unix_addr.sun_path, NULL);
> +
> + EXPECT_NE(NULL, absolute_path)
> + {
> + TH_LOG("realpath() failed: %s", strerror(errno));
> + return;
> + }
> +
> + log_match_cursor =
> + stpncpy(log_match,
> + REGEX_LANDLOCK_PREFIX
> + " blockers=scope\\.pathname_unix_socket path=\"",
> + log_match_remaining);
> + log_match_remaining =
> + sizeof(log_match) - (log_match_cursor - log_match);
> + ASSERT_NE(0, log_match_remaining);
> + log_match_cursor = regex_escape(absolute_path, log_match_cursor,
> + log_match_remaining);
> + free(absolute_path);
> + if (log_match_cursor < 0) {
> + TH_LOG("regex_escape() failed (buffer too small)");
> + return;
> + }
> + log_match_remaining =
> + sizeof(log_match) - (log_match_cursor - log_match);
> + ASSERT_NE(0, log_match_remaining);
> + log_match_cursor =
> + stpncpy(log_match_cursor, "\"$", log_match_remaining);
> + log_match_remaining =
> + sizeof(log_match) - (log_match_cursor - log_match);
> + ASSERT_NE(0, log_match_remaining);
> + }
> +
> + EXPECT_EQ(0, audit_match_record(self->audit_fd, AUDIT_LANDLOCK_ACCESS,
> + log_match, NULL));
>
> ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
> EXPECT_EQ(0, close(dgram_client));
> --
> 2.52.0
>
>
More information about the Linux-security-module-archive
mailing list