* [PATCH RFC 1/3] selftests/landlock: move sandbox_type to common
2025-07-19 12:41 [PATCH RFC 0/3] selftests/landlock: scoping abstractions Abhinav Saxena
@ 2025-07-19 12:41 ` Abhinav Saxena
2025-07-19 12:41 ` [PATCH RFC 2/3] selftests/landlock: add cross-domain variants Abhinav Saxena
2025-07-19 12:41 ` [PATCH RFC 3/3] selftests/landlock: add cross-domain signal tests Abhinav Saxena
2 siblings, 0 replies; 4+ messages in thread
From: Abhinav Saxena @ 2025-07-19 12:41 UTC (permalink / raw)
To: Mickaël Salaün, Günther Noack, Shuah Khan
Cc: linux-security-module, linux-kselftest, linux-kernel,
Abhinav Saxena
The enum sandbox_type describes three execution modes for Landlock
test tasks:
- NO_SANDBOX: no Landlock domain
- SCOPE_SANDBOX: scoped Landlock domain
- OTHER_SANDBOX: placeholder for future cases
This enum was defined in scoped_multiple_domain_variants.h but is
needed by upcoming cross-domain test variants. Rather than duplicate
the definition, move it to scoped_common.h which is already included
by all scope-related tests.
This is a pure refactor with no functional changes to test binaries.
Signed-off-by: Abhinav Saxena <xandfury@gmail.com>
---
tools/testing/selftests/landlock/scoped_common.h | 7 +++++++
tools/testing/selftests/landlock/scoped_multiple_domain_variants.h | 7 -------
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/tools/testing/selftests/landlock/scoped_common.h b/tools/testing/selftests/landlock/scoped_common.h
index a9a912d30c4d..08c7d732650c 100644
--- a/tools/testing/selftests/landlock/scoped_common.h
+++ b/tools/testing/selftests/landlock/scoped_common.h
@@ -9,6 +9,13 @@
#include <sys/types.h>
+enum sandbox_type {
+ NO_SANDBOX,
+ SCOPE_SANDBOX,
+ /* Any other type of sandboxing domain */
+ OTHER_SANDBOX,
+};
+
static void create_scoped_domain(struct __test_metadata *const _metadata,
const __u16 scope)
{
diff --git a/tools/testing/selftests/landlock/scoped_multiple_domain_variants.h b/tools/testing/selftests/landlock/scoped_multiple_domain_variants.h
index bcd9a83805d0..23022c6ebece 100644
--- a/tools/testing/selftests/landlock/scoped_multiple_domain_variants.h
+++ b/tools/testing/selftests/landlock/scoped_multiple_domain_variants.h
@@ -5,13 +5,6 @@
* Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com>
*/
-enum sandbox_type {
- NO_SANDBOX,
- SCOPE_SANDBOX,
- /* Any other type of sandboxing domain */
- OTHER_SANDBOX,
-};
-
/* clang-format on */
FIXTURE_VARIANT(scoped_vs_unscoped)
{
--
2.43.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH RFC 2/3] selftests/landlock: add cross-domain variants
2025-07-19 12:41 [PATCH RFC 0/3] selftests/landlock: scoping abstractions Abhinav Saxena
2025-07-19 12:41 ` [PATCH RFC 1/3] selftests/landlock: move sandbox_type to common Abhinav Saxena
@ 2025-07-19 12:41 ` Abhinav Saxena
2025-07-19 12:41 ` [PATCH RFC 3/3] selftests/landlock: add cross-domain signal tests Abhinav Saxena
2 siblings, 0 replies; 4+ messages in thread
From: Abhinav Saxena @ 2025-07-19 12:41 UTC (permalink / raw)
To: Mickaël Salaün, Günther Noack, Shuah Khan
Cc: linux-security-module, linux-kselftest, linux-kernel,
Abhinav Saxena
Add scoped_cross_domain_variants.h providing shared test variants for
interactions between two independent Landlock domains. Current tests
only cover hierarchical (parent-child) relationships but miss the
case where unrelated processes establish peer domains.
The header defines four canonical variants:
- none_to_none: both processes unrestricted
- none_to_scoped: only accessor process scoped
- scoped_to_none: only resource process scoped
- scoped_to_scoped: both processes scoped (peer domains)
This abstraction will be shared across signal, abstract UNIX socket,
and future scope types (like memfd execution) to ensure comprehensive
cross-domain test coverage.
Signed-off-by: Abhinav Saxena <xandfury@gmail.com>
---
.../landlock/scoped_cross_domain_variants.h | 54 ++++++++++++++++++++++
1 file changed, 54 insertions(+)
diff --git a/tools/testing/selftests/landlock/scoped_cross_domain_variants.h b/tools/testing/selftests/landlock/scoped_cross_domain_variants.h
new file mode 100644
index 000000000000..6068987a52c8
--- /dev/null
+++ b/tools/testing/selftests/landlock/scoped_cross_domain_variants.h
@@ -0,0 +1,54 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Landlock self-tests - cross-domain scope variants
+ *
+ * Provides one FIXTURE_VARIANT and the four canonical combinations
+ * (none->none, none->scoped, scoped->none, scoped->scoped). Every test that
+ * checks interactions between two independently created domains
+ * includes this header and iterates over the variants.
+ *
+ * Variant structure: which domain each side of the interaction lives in.
+ * resource_domain - process that creates/owns the resource
+ * accessor_domain - process that uses the resource
+ *
+ * Copyright © 2025 Abhinav Saxena <xandfury@gmail.com>
+ *
+ */
+
+FIXTURE_VARIANT(cross_domain_scope)
+{
+ enum sandbox_type resource_domain;
+ enum sandbox_type accessor_domain;
+};
+
+/* Four concrete combinations */
+FIXTURE_VARIANT_ADD(cross_domain_scope, none_to_none) {
+ .resource_domain = NO_SANDBOX,
+ .accessor_domain = NO_SANDBOX,
+};
+
+FIXTURE_VARIANT_ADD(cross_domain_scope, none_to_scoped) {
+ .resource_domain = NO_SANDBOX,
+ .accessor_domain = SCOPE_SANDBOX,
+};
+
+FIXTURE_VARIANT_ADD(cross_domain_scope, scoped_to_none) {
+ .resource_domain = SCOPE_SANDBOX,
+ .accessor_domain = NO_SANDBOX,
+};
+
+FIXTURE_VARIANT_ADD(cross_domain_scope, scoped_to_scoped) {
+ .resource_domain = SCOPE_SANDBOX,
+ .accessor_domain = SCOPE_SANDBOX,
+};
+
+/*
+ * Mapping reminder:
+ * SIGNAL resource = receiver accessor = sender
+ * ABSTRACT UNIX resource = server accessor = client
+ * future scopes resource = creator accessor = user
+ *
+ * Only the accessor domain is enforced; tests therefore expect:
+ * accessor NO_SANDBOX -> ALLOW operation
+ * accessor SCOPE_SANDBOX -> DENY if resource is outside its domain
+ */
--
2.43.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH RFC 3/3] selftests/landlock: add cross-domain signal tests
2025-07-19 12:41 [PATCH RFC 0/3] selftests/landlock: scoping abstractions Abhinav Saxena
2025-07-19 12:41 ` [PATCH RFC 1/3] selftests/landlock: move sandbox_type to common Abhinav Saxena
2025-07-19 12:41 ` [PATCH RFC 2/3] selftests/landlock: add cross-domain variants Abhinav Saxena
@ 2025-07-19 12:41 ` Abhinav Saxena
2 siblings, 0 replies; 4+ messages in thread
From: Abhinav Saxena @ 2025-07-19 12:41 UTC (permalink / raw)
To: Mickaël Salaün, Günther Noack, Shuah Khan
Cc: linux-security-module, linux-kselftest, linux-kernel,
Abhinav Saxena
Add cross_domain_signal test using the new cross-domain variants to
validate signal delivery between independent peer domains. This fills
a gap in current test coverage which only exercises hierarchical
domain relationships.
The test creates a fork tree where both children call
landlock_restrict_self() for the first time, ensuring their
domain->parent pointers are NULL and creating true peer domains:
coordinator (no domain)
|
+-- resource_proc (Domain X) /* owns the resource */
|
+-- accessor_proc (Domain Y) /* tries to access */
Tests verify that kill(SIGUSR1) behaves correctly across all four
domain combinations, with scoped accessors properly denied (-EPERM)
when attempting cross-domain signal delivery.
This establishes the resource-accessor test pattern for future scope
types where Landlock restrictions apply only to the accessor side.
Signed-off-by: Abhinav Saxena <xandfury@gmail.com>
---
.../selftests/landlock/scoped_signal_test.c | 237 +++++++++++++++++++++
1 file changed, 237 insertions(+)
diff --git a/tools/testing/selftests/landlock/scoped_signal_test.c b/tools/testing/selftests/landlock/scoped_signal_test.c
index d8bf33417619..b52eaf1f3c0a 100644
--- a/tools/testing/selftests/landlock/scoped_signal_test.c
+++ b/tools/testing/selftests/landlock/scoped_signal_test.c
@@ -559,4 +559,241 @@ TEST_F(fown, sigurg_socket)
_metadata->exit_code = KSFT_FAIL;
}
+FIXTURE(cross_domain_scope)
+{
+ int coordinator_to_resource_pipe[2]; /* coordinator -> resource sync */
+ int coordinator_to_accessor_pipe[2]; /* coordinator -> accessor sync */
+ int result_pipe[2]; /* accessor -> coordinator result */
+ pid_t resource_pid; /* Domain X process */
+ pid_t accessor_pid; /* Domain Y process */
+};
+
+/* Include the cross-domain variants */
+#include "scoped_cross_domain_variants.h"
+
+FIXTURE_SETUP(cross_domain_scope)
+{
+ drop_caps(_metadata);
+ /* Create communication channels */
+ ASSERT_EQ(0, pipe2(self->coordinator_to_resource_pipe, O_CLOEXEC));
+ ASSERT_EQ(0, pipe2(self->coordinator_to_accessor_pipe, O_CLOEXEC));
+ ASSERT_EQ(0, pipe2(self->result_pipe, O_CLOEXEC));
+
+ signal_received = 0; /* Reset for each test */
+ self->resource_pid = -1;
+ self->accessor_pid = -1;
+}
+
+FIXTURE_TEARDOWN(cross_domain_scope)
+{
+ close(self->coordinator_to_resource_pipe[0]);
+ close(self->coordinator_to_resource_pipe[1]);
+ close(self->coordinator_to_accessor_pipe[0]);
+ close(self->coordinator_to_accessor_pipe[1]);
+ close(self->result_pipe[0]);
+ close(self->result_pipe[1]);
+}
+
+static void cross_domain_signal_handler(int sig)
+{
+ if (sig == SIGUSR1 || sig == SIGURG)
+ signal_received = 1;
+ else if (sig == SIGALRM)
+ signal_received = 2; /* Alarm timeout */
+}
+
+/*
+ * Maybe this should go into common.h or scoped_common.h so that
+ * we can perhaps test interactions b/w different types of sanboxes
+ */
+static void create_independent_domain(struct __test_metadata *_metadata,
+ enum sandbox_type domain_type,
+ const char *process_role)
+{
+ if (domain_type == SCOPE_SANDBOX) {
+ /*
+ * This is the critical call - first landlock_restrict_self()
+ * ensures domain->parent == NULL, creating true peer domains
+ */
+ create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
+ }
+}
+
+TEST_F(cross_domain_scope, cross_domain_signal)
+{
+ enum sandbox_type resource_domain = variant->resource_domain;
+ enum sandbox_type accessor_domain = variant->accessor_domain;
+
+ TH_LOG("Resource domain: %s",
+ resource_domain == NO_SANDBOX ? "unrestricted" : "scoped");
+ TH_LOG("Accessor domain: %s",
+ accessor_domain == NO_SANDBOX ? "unrestricted" : "scoped");
+ /*
+ * Fork tree:
+ * coordinator (no domain)
+ * ├── resource_proc (Domain X)
+ * └── accessor_proc (Domain Y)
+ */
+
+ /* === RESOURCE PROCESS (Domain X) === */
+ self->resource_pid = fork();
+ ASSERT_GE(self->resource_pid, 0);
+
+ if (self->resource_pid == 0) {
+ /* Close unused pipe ends */
+
+ /* Don't write to coordinator */
+ close(self->coordinator_to_resource_pipe[1]);
+ /* Don't read accessor pipe */
+ close(self->coordinator_to_accessor_pipe[0]);
+ /* Don't write accessor pipe */
+ close(self->coordinator_to_accessor_pipe[1]);
+ close(self->result_pipe[0]); /* Don't read results */
+ close(self->result_pipe[1]); /* Don't write results */
+
+ /* Create independent domain */
+ create_independent_domain(_metadata, resource_domain,
+ "RESOURCE");
+
+ /* Install signal handler */
+ struct sigaction sa = {
+ .sa_handler = cross_domain_signal_handler,
+ .sa_flags = SA_RESTART
+ };
+
+ sigemptyset(&sa.sa_mask);
+ ASSERT_EQ(0, sigaction(SIGUSR1, &sa, NULL));
+ ASSERT_EQ(0, sigaction(SIGALRM, &sa, NULL));
+
+ /* Wait for coordinator signal to start */
+ char sync_byte;
+ ssize_t ret = read(self->coordinator_to_resource_pipe[0],
+ &sync_byte, 1);
+ ASSERT_EQ(1, ret);
+ close(self->coordinator_to_resource_pipe[0]);
+
+ /* Set timeout and wait for signal */
+ alarm(3);
+ pause();
+
+ /*
+ * Exit based on what signal was received
+ * 0=success, 1=timeout/failure
+ */
+ _exit(signal_received == 1 ? 0 : 1);
+ }
+
+ /* === ACCESSOR PROCESS (Domain Y) === */
+ self->accessor_pid = fork();
+ ASSERT_GE(self->accessor_pid, 0);
+
+ if (self->accessor_pid == 0) {
+ /* Close unused pipe ends */
+
+ /* Don't read resource pipe */
+ close(self->coordinator_to_resource_pipe[0]);
+ /* Don't write resource pipe */
+ close(self->coordinator_to_resource_pipe[1]);
+ /* Don't write to coordinator */
+ close(self->coordinator_to_accessor_pipe[1]);
+ close(self->result_pipe[0]); /* Don't read results */
+
+ create_independent_domain(_metadata, accessor_domain,
+ "ACCESSOR");
+
+ /* Wait for coordinator to signal start */
+ char sync_byte;
+ ssize_t ret = read(self->coordinator_to_accessor_pipe[0],
+ &sync_byte, 1);
+ ASSERT_EQ(1, ret);
+ close(self->coordinator_to_accessor_pipe[0]);
+
+ /* 200ms delay to ensure resource is in pause() */
+ usleep(200000);
+
+ /* Attempt cross-domain signal - this is the core test */
+ int kill_result = kill(self->resource_pid, SIGUSR1);
+ int kill_errno = errno;
+
+ /* Send results back to coordinator */
+ struct {
+ int result;
+ int error;
+ } test_result = { kill_result, kill_errno };
+
+ ret = write(self->result_pipe[1], &test_result,
+ sizeof(test_result));
+ ASSERT_EQ(sizeof(test_result), ret);
+ close(self->result_pipe[1]);
+
+ _exit(0);
+ }
+
+ /* === COORDINATOR PROCESS (No domain) === */
+
+ /* Close unused pipe ends */
+ close(self->coordinator_to_resource_pipe[0]); /* Don't read from resource */
+ close(self->coordinator_to_accessor_pipe[0]); /* Don't read from accessor */
+ close(self->result_pipe[1]); /* Don't write results */
+
+ /* Give processes time to set up domains */
+ usleep(100000); /* 100ms */
+
+ /* Signal both processes to start the test */
+ char go_signal = '1';
+
+ ASSERT_EQ(1,
+ write(self->coordinator_to_resource_pipe[1], &go_signal, 1));
+
+ ASSERT_EQ(1,
+ write(self->coordinator_to_accessor_pipe[1], &go_signal, 1));
+
+ close(self->coordinator_to_resource_pipe[1]);
+ close(self->coordinator_to_accessor_pipe[1]);
+
+ /* Collect accessor results */
+ struct {
+ int result;
+ int error;
+ } test_result;
+
+ ssize_t ret =
+ read(self->result_pipe[0], &test_result, sizeof(test_result));
+ ASSERT_EQ(sizeof(test_result), ret);
+ close(self->result_pipe[0]);
+
+ /* Wait for both processes to complete */
+ int accessor_status, resource_status;
+
+ /* Accessor should always exit cleanly */
+ ASSERT_EQ(self->accessor_pid,
+ waitpid(self->accessor_pid, &accessor_status, 0));
+
+ ASSERT_EQ(self->resource_pid,
+ waitpid(self->resource_pid, &resource_status, 0));
+
+ EXPECT_EQ(0, WEXITSTATUS(accessor_status));
+ /* Determine expected behavior based on your table */
+ bool should_succeed = (accessor_domain == NO_SANDBOX);
+
+ if (should_succeed) {
+ /* Signal should succeed across domains */
+ EXPECT_EQ(0, test_result.result); /* kill() succeeds */
+ /* resource receives signal */
+ EXPECT_EQ(0, WEXITSTATUS(resource_status));
+ } else {
+ /* Signal should be blocked by cross-domain isolation */
+ EXPECT_EQ(-1, test_result.result); /* kill() fails */
+ EXPECT_EQ(EPERM, test_result.error); /* with EPERM */
+ /* resource times out */
+ EXPECT_NE(0, WEXITSTATUS(resource_status));
+ }
+}
+
+/* Test for socket-based signals (SIGURG) across independent domains */
+TEST_F(cross_domain_scope, DISABLED_file_signal_cross_domain)
+{
+ SKIP(return, "Skip for now");
+}
+
TEST_HARNESS_MAIN
--
2.43.0
^ permalink raw reply related [flat|nested] 4+ messages in thread