linux-security-module.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH RFC 0/3] selftests/landlock: scoping abstractions
@ 2025-07-19 12:41 Abhinav Saxena
  2025-07-19 12:41 ` [PATCH RFC 1/3] selftests/landlock: move sandbox_type to common Abhinav Saxena
                   ` (2 more replies)
  0 siblings, 3 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

Hi all,

I was starting to work on the memfd-exec[1] feature and observed that
Landlock's scoped-IPC features (abstract UNIX sockets and signals)
follow a consistent high-level model, which I'm calling a
resource-accessor pattern:

Resource Process <-> Accessor Process
    - Resource process: owns or manages the asset
        - socket creator (bind/accept)
        - signal handler  
        - memfd creator
    - Accessor process: attempts to use the asset
        - socket client (connect/sendto)
        - signal sender
        - memfd executor

RESOURCE-ACCESSOR PATTERN FUNDAMENTALS
======================================

This pattern appears fundamental to Landlock scoping because:

1. Consistent enforcement model: Landlock restrictions are enforced
   only on the accessor side; the resource side remains unconstrained
   across all scope types.

2. Reflects actual security boundaries: In practice, sandboxed
   processes typically need to access resources created by other
   processes, not the reverse.

3. Scalable design: This model works consistently whether processes
   are in parent-child relationships or independent peer domains.

4. Real-world usage patterns: Container runtimes and sandbox
   orchestrators routinely start multiple workers that restrict
   themselves independently.

CURRENT TEST COVERAGE GAP
=========================

Existing self-tests cover hierarchical resource <-> accessor pairs
but do not exercise the case where each task enters an independent
domain. While 'sibling_domain' tests exist, they still use
parent-child relationship patterns rather than true peer domains.

Current Coverage (Linear Hierarchies Only):
-------------------------------------------

Type 1: Parent-Child (scoped_domains)
   P1 ---- P2

Type 2: Three Generations (scoped_vs_unscoped)  
   P1 ---- P2 ---- P3

Variations tested for both types:
- No domains
- Various scoped domain combinations  
- Nested domains within inherited domains
- Mixed domain types (SCOPE vs OTHER vs NONE)

Missing Coverage (True Sibling Scenarios):
------------------------------------------

Root
 |
 +-- Child A [various domain types]
 |
 +-- Child B [various domain types]

Missing test scenarios:
- A <-> B cross-sibling communication
- Mixed sibling domain combinations
- Sibling isolation enforcement
- Parent -> A, Parent -> B differential access

SOLUTION
========

This series implements the missing sibling pattern using the
resource-accessor model. The tests create a fork tree that looks
like this:

    coordinator (no domain)  
    |
    +-- resource_proc (Domain X) /* owns the resource */  
    |
    +-- accessor_proc (Domain Y) /* tries to access */

This directly addresses the missing coverage by creating two
independent child processes that establish peer domains, rather than
the hierarchical parent-child domains covered by existing tests.

Both children call landlock_restrict_self() for the first time, so
their struct landlock_domain->parent pointers are NULL, creating
true peer domains. The harness exposes four test variants:

Variant name       | Resource domain | Accessor domain | Result   
-------------------|-----------------|-----------------|----------
none_to_none       | none            | none            | ALLOW    
none_to_scoped     | none            | scoped          | DENY     
scoped_to_none     | scoped          | none            | ALLOW    
scoped_to_scoped   | scoped          | scoped (peer)   | DENY

The scoped_to_scoped case was missing from current coverage.

TESTING
=======

All patches apply cleanly to v6.14-rc2 and pass on landlock/master.
The helpers are small and re-use the existing kselftest_harness.h
fixture/variant pattern. All patches have been validated with
scripts/checkpatch.pl --strict and show no warnings.

This series introduces **no kernel changes**, only selftests additions.

Feedback very welcome.

Thanks,
Abhinav

[1] https://github.com/landlock-lsm/linux/issues/37

Links:
- Landlock documentation: https://docs.kernel.org/userspace-api/landlock.html
- Landlock LSM kernel docs: https://docs.kernel.org/security/landlock.html
- Existing tests: tools/testing/selftests/landlock/scoped_*

Signed-off-by: Abhinav Saxena <xandfury@gmail.com>
---
Abhinav Saxena (3):
      selftests/landlock: move sandbox_type to common
      selftests/landlock: add cross-domain variants
      selftests/landlock: add cross-domain signal tests

 tools/testing/selftests/landlock/scoped_common.h   |   7 +
 .../landlock/scoped_cross_domain_variants.h        |  54 +++++
 .../landlock/scoped_multiple_domain_variants.h     |   7 -
 .../selftests/landlock/scoped_signal_test.c        | 237 +++++++++++++++++++++
 4 files changed, 298 insertions(+), 7 deletions(-)
---
base-commit: 5b74b2eff1eeefe43584e5b7b348c8cd3b723d38
change-id: 20250715-landlock_abstractions-dbc0aabf1063

Best regards,
-- 
Abhinav Saxena <xandfury@gmail.com>


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [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

end of thread, other threads:[~2025-07-19 12:41 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH RFC 3/3] selftests/landlock: add cross-domain signal tests Abhinav Saxena

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).