linux-security-module.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH v2 0/6] Landlock: Implement scope control for pathname Unix sockets
@ 2025-12-30 17:20 Tingmao Wang
  2025-12-30 17:20 ` [PATCH v2 1/6] landlock: Add LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET scope bit to uAPI Tingmao Wang
                   ` (6 more replies)
  0 siblings, 7 replies; 9+ messages in thread
From: Tingmao Wang @ 2025-12-30 17:20 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Tingmao Wang, Günther Noack, Demi Marie Obenour, Alyssa Ross,
	Jann Horn, Tahera Fahimi, Justin Suess, linux-security-module

Changes in v2:
  Fix grammar in doc, rebased on mic/next, and extracted common code from
  hook_unix_stream_connect and hook_unix_may_send into a separate
  function.

The rest is the same as the v1 cover letter:

This patch series extend the existing abstract Unix socket scoping to
pathname (i.e. normal file-based) sockets as well, by adding a new scope
bit LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET that works the same as
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, except that restricts pathname Unix
sockets.  This means that a sandboxed process with this scope enabled will
not be able to connect to Unix sockets created outside the sandbox via the
filesystem.

There is a future plan [1] for allowing specific sockets based on FS
hierarchy, but this series is only determining access based on domain
parent-child relationship.  There is currently no way to allow specific
(outside the Landlock domain) Unix sockets, and none of the existing
Landlock filesystem controls apply to socket connect().

With this series, we can now properly protect against things like the the
following while only relying on Landlock:

    (running under tmux)
    root@6-19-0-rc1-dev-00023-g68f0b276cbeb ~# LL_FS_RO=/ LL_FS_RW= ./sandboxer bash
    Executing the sandboxed command...
    root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi
    cat: /tmp/hi: No such file or directory
    root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# tmux new-window 'echo hi > /tmp/hi'
    root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi
    hi

The above but with Unix socket scoping enabled (both pathname and abstract
sockets) - the sandboxed shell can now no longer talk to tmux due to the
socket being created from outside the Landlock sandbox:

    (running under tmux)
    root@6-19-0-rc1-dev-00023-g68f0b276cbeb ~# LL_FS_RO=/ LL_FS_RW= LL_SCOPED=u:a ./sandboxer bash
    Executing the sandboxed command...
    root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi
    cat: /tmp/hi: No such file or directory
    root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# tmux new-window 'echo hi > /tmp/hi'
    error connecting to /tmp/tmux-0/default (Operation not permitted)
    root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi
    cat: /tmp/hi: No such file or directory

Tmux is just one example.  In a standard systemd session, `systemd-run
--user` can also be used (--user will run the command in the user's
session, without requiring any root privileges), and likely a lot more if
running in a desktop environment with many popular applications.  This
change therefore makes it possible to create sandboxes without relying on
additional mechanisms like seccomp to protect against such issues.

These kind of issues was originally discussed on here (I took the idea for
systemd-run from Demi):
https://spectrum-os.org/lists/archives/spectrum-devel/00256266-26db-40cf-8f5b-f7c7064084c2@gmail.com/

Demo with socat + sandboxer:

Outside:
    socat unix-listen:/foo.sock,fork -

Sandbox with pathname socket scope bit:
    root@6-19-0-rc1-dev-00023-g0994a10d6512 ~# LL_FS_RW=/ LL_FS_RO= LL_SCOPED=u /sandboxer socat -d2 unix:/foo.sock -
    Executing the sandboxed command...
    2025/12/27 20:28:54 socat[1227] E UNIX-CLIENT: /foo.sock: Operation not permitted
    2025/12/27 20:28:54 socat[1227] N exit(1)

Sandbox without pathname socket scope bit:
    root@6-19-0-rc1-dev-00023-g0994a10d6512 ~# LL_FS_RW=/ LL_FS_RO= LL_SCOPED= /sandboxer socat -d2 unix:/foo.sock -
    Executing the sandboxed command...
    2025/12/27 20:29:22 socat[1250] N successfully connected from local address AF=1 "(7\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xB0\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xC3\xAE\xAE\xAE\xAE"
    ...

Sandbox with only abstract socket scope bit:
    root@6-19-0-rc1-dev-00023-g0994a10d6512 ~# LL_FS_RW=/ LL_FS_RO= LL_SCOPED=a /sandboxer socat -d2 unix:/foo.sock -
    Executing the sandboxed command...
    2025/12/27 20:29:26 socat[1259] N successfully connected from local address AF=1 "\0\0\0\0\0\0\0\0\0"
    ...

Sendmsg/recvmsg - outside:
    socat unix-recvfrom:/datagram.sock -

Sandbox with pathname socket scope bit:
    root@6-19-0-rc1-dev-00023-g0994a10d6512 ~# LL_FS_RW=/ LL_FS_RO= LL_SCOPED=u /sandboxer socat -d2 unix-sendto:/datagram.sock -
    Executing the sandboxed command...
    ...
    2025/12/27 20:33:04 socat[1446] N starting data transfer loop with FDs [5,5] and [0,1]
    123
    2025/12/27 20:33:05 socat[1446] E sendto(5, 0x55d260d8f000, 4, 0, AF=1 "/datagram.sock", 16): Operation not permitted
    2025/12/27 20:33:05 socat[1446] N exit(1)

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

Closes: https://github.com/landlock-lsm/linux/issues/51

Tingmao Wang (6):
  landlock: Add LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET scope bit to uAPI
  landlock: Implement LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
  samples/landlock: Support LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
  selftests/landlock: Support pathname socket path in set_unix_address
  selftests/landlock: Repurpose scoped_abstract_unix_test.c for pathname
    sockets too.
  selftests/landlock: Add pathname socket variants for more tests

 Documentation/userspace-api/landlock.rst      |  37 +-
 include/uapi/linux/landlock.h                 |   8 +-
 samples/landlock/sandboxer.c                  |  23 +-
 security/landlock/audit.c                     |   4 +
 security/landlock/audit.h                     |   1 +
 security/landlock/limits.h                    |   2 +-
 security/landlock/syscalls.c                  |   2 +-
 security/landlock/task.c                      | 109 ++-
 tools/testing/selftests/landlock/base_test.c  |   2 +-
 tools/testing/selftests/landlock/common.h     |  33 +-
 tools/testing/selftests/landlock/net_test.c   |   2 +-
 .../selftests/landlock/scoped_signal_test.c   |   2 +-
 .../testing/selftests/landlock/scoped_test.c  |   2 +-
 ...bstract_unix_test.c => scoped_unix_test.c} | 855 ++++++++++++------
 14 files changed, 757 insertions(+), 325 deletions(-)
 rename tools/testing/selftests/landlock/{scoped_abstract_unix_test.c => scoped_unix_test.c} (51%)


base-commit: ef4536f15224418b327a7b5d5cae07dab042760f
-- 
2.52.0


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

* [PATCH v2 1/6] landlock: Add LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET scope bit to uAPI
  2025-12-30 17:20 [PATCH v2 0/6] Landlock: Implement scope control for pathname Unix sockets Tingmao Wang
@ 2025-12-30 17:20 ` Tingmao Wang
  2025-12-30 17:20 ` [PATCH v2 2/6] landlock: Implement LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET Tingmao Wang
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: Tingmao Wang @ 2025-12-30 17:20 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Tingmao Wang, Günther Noack, Demi Marie Obenour, Alyssa Ross,
	Jann Horn, Tahera Fahimi, Justin Suess, linux-security-module

Add the new scope bit to the uAPI header, add documentation, and bump ABI
version to 8.

This documentation edit specifically calls out the security implications of
not restricting sockets.

Fix some minor cosmetic issue in landlock.h around the changed lines as
well.

Signed-off-by: Tingmao Wang <m@maowtm.org>
---

Changes in v2:
- Fix grammar

Note that in the code block in "Defining and enforcing a security policy"
the switch case currently jumps from 5 to 7.  This should be fixed by
https://lore.kernel.org/all/20251216210248.4150777-1-samasth.norway.ananda@oracle.com/

 Documentation/userspace-api/landlock.rst      | 37 ++++++++++++++++---
 include/uapi/linux/landlock.h                 |  8 +++-
 security/landlock/limits.h                    |  2 +-
 security/landlock/syscalls.c                  |  2 +-
 tools/testing/selftests/landlock/base_test.c  |  2 +-
 .../testing/selftests/landlock/scoped_test.c  |  2 +-
 6 files changed, 42 insertions(+), 11 deletions(-)

diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
index 1d0c2c15c22e..5620a2be1091 100644
--- a/Documentation/userspace-api/landlock.rst
+++ b/Documentation/userspace-api/landlock.rst
@@ -83,7 +83,8 @@ to be explicit about the denied-by-default access rights.
             LANDLOCK_ACCESS_NET_CONNECT_TCP,
         .scoped =
             LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
-            LANDLOCK_SCOPE_SIGNAL,
+            LANDLOCK_SCOPE_SIGNAL |
+            LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET,
     };
 
 Because we may not know which kernel version an application will be executed
@@ -127,6 +128,10 @@ version, and only use the available subset of access rights:
         /* Removes LANDLOCK_SCOPE_* for ABI < 6 */
         ruleset_attr.scoped &= ~(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
                                  LANDLOCK_SCOPE_SIGNAL);
+        __attribute__((fallthrough));
+    case 7:
+        /* Removes LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET for ABI < 8 */
+        ruleset_attr.scoped &= ~LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
     }
 
 This enables the creation of an inclusive ruleset that will contain our rules.
@@ -328,10 +333,15 @@ The operations which can be scoped are:
     This limits the sending of signals to target processes which run within the
     same or a nested Landlock domain.
 
-``LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET``
-    This limits the set of abstract :manpage:`unix(7)` sockets to which we can
-    :manpage:`connect(2)` to socket addresses which were created by a process in
-    the same or a nested Landlock domain.
+``LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET`` and ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET``
+    This limits the set of :manpage:`unix(7)` sockets to which we can
+    :manpage:`connect(2)` to socket addresses which were created by a
+    process in the same or a nested Landlock domain.
+    ``LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET`` applies to abstract sockets,
+    and ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` applies to pathname
+    sockets.  Even though pathname sockets are represented in the
+    filesystem, Landlock filesystem rules do not currently control access
+    to them.
 
     A :manpage:`sendto(2)` on a non-connected datagram socket is treated as if
     it were doing an implicit :manpage:`connect(2)` and will be blocked if the
@@ -604,6 +614,23 @@ Landlock audit events with the ``LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF``,
 sys_landlock_restrict_self().  See Documentation/admin-guide/LSM/landlock.rst
 for more details on audit.
 
+Pathname UNIX socket (ABI < 8)
+------------------------------
+
+Starting with the Landlock ABI version 8, it is possible to restrict
+connections to a pathname (non-abstract) :manpage:`unix(7)` socket by
+setting ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` to the ``scoped`` ruleset
+attribute.  This works the same way as the abstract socket scoping.
+
+This allows sandboxing applications using only Landlock to protect against
+bypasses relying on connecting to Unix sockets of other services running
+under the same user.  These services typically assume that any process
+capable of connecting to a local Unix socket, or connecting with the
+expected user credentials, is trusted.  Without this protection, sandbox
+escapes may be possible, especially when running in a standard desktop
+environment, such as by using systemd-run, or sockets exposed by other
+common applications.
+
 .. _kernel_support:
 
 Kernel support
diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index f030adc462ee..590c6d4171a0 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -364,10 +364,14 @@ struct landlock_net_port_attr {
  *   related Landlock domain (e.g., a parent domain or a non-sandboxed process).
  * - %LANDLOCK_SCOPE_SIGNAL: Restrict a sandboxed process from sending a signal
  *   to another process outside the domain.
+ * - %LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET: Restrict a sandboxed process from
+ *   connecting to a pathname UNIX socket created by a process outside the
+ *   related Landlock domain.
  */
 /* clang-format off */
 #define LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET		(1ULL << 0)
-#define LANDLOCK_SCOPE_SIGNAL		                (1ULL << 1)
-/* clang-format on*/
+#define LANDLOCK_SCOPE_SIGNAL				(1ULL << 1)
+#define LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET		(1ULL << 2)
+/* clang-format on */
 
 #endif /* _UAPI_LINUX_LANDLOCK_H */
diff --git a/security/landlock/limits.h b/security/landlock/limits.h
index 65b5ff051674..d653e14dba10 100644
--- a/security/landlock/limits.h
+++ b/security/landlock/limits.h
@@ -27,7 +27,7 @@
 #define LANDLOCK_MASK_ACCESS_NET	((LANDLOCK_LAST_ACCESS_NET << 1) - 1)
 #define LANDLOCK_NUM_ACCESS_NET		__const_hweight64(LANDLOCK_MASK_ACCESS_NET)
 
-#define LANDLOCK_LAST_SCOPE		LANDLOCK_SCOPE_SIGNAL
+#define LANDLOCK_LAST_SCOPE		LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
 #define LANDLOCK_MASK_SCOPE		((LANDLOCK_LAST_SCOPE << 1) - 1)
 #define LANDLOCK_NUM_SCOPE		__const_hweight64(LANDLOCK_MASK_SCOPE)
 
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 0116e9f93ffe..66fd196be85a 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -161,7 +161,7 @@ static const struct file_operations ruleset_fops = {
  * Documentation/userspace-api/landlock.rst should be updated to reflect the
  * UAPI change.
  */
-const int landlock_abi_version = 7;
+const int landlock_abi_version = 8;
 
 /**
  * sys_landlock_create_ruleset - Create a new ruleset
diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
index 7b69002239d7..f4b1a275d8d9 100644
--- a/tools/testing/selftests/landlock/base_test.c
+++ b/tools/testing/selftests/landlock/base_test.c
@@ -76,7 +76,7 @@ TEST(abi_version)
 	const struct landlock_ruleset_attr ruleset_attr = {
 		.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
 	};
-	ASSERT_EQ(7, landlock_create_ruleset(NULL, 0,
+	ASSERT_EQ(8, landlock_create_ruleset(NULL, 0,
 					     LANDLOCK_CREATE_RULESET_VERSION));
 
 	ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
diff --git a/tools/testing/selftests/landlock/scoped_test.c b/tools/testing/selftests/landlock/scoped_test.c
index b90f76ed0d9c..7f83512a328d 100644
--- a/tools/testing/selftests/landlock/scoped_test.c
+++ b/tools/testing/selftests/landlock/scoped_test.c
@@ -12,7 +12,7 @@
 
 #include "common.h"
 
-#define ACCESS_LAST LANDLOCK_SCOPE_SIGNAL
+#define ACCESS_LAST LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
 
 TEST(ruleset_with_unknown_scope)
 {
-- 
2.52.0

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

* [PATCH v2 2/6] landlock: Implement LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
  2025-12-30 17:20 [PATCH v2 0/6] Landlock: Implement scope control for pathname Unix sockets Tingmao Wang
  2025-12-30 17:20 ` [PATCH v2 1/6] landlock: Add LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET scope bit to uAPI Tingmao Wang
@ 2025-12-30 17:20 ` Tingmao Wang
  2025-12-30 17:20 ` [PATCH v2 3/6] samples/landlock: Support LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET Tingmao Wang
                   ` (4 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: Tingmao Wang @ 2025-12-30 17:20 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Tingmao Wang, Günther Noack, Demi Marie Obenour, Alyssa Ross,
	Jann Horn, Tahera Fahimi, Justin Suess, linux-security-module

Extend the existing abstract UNIX socket scoping to pathname sockets as
well.  Basically all of the logic is reused between the two types, just
that pathname sockets scoping are controlled by another bit, and has its
own audit request type (since the current one is named
"abstract_unix_socket").

Closes: https://github.com/landlock-lsm/linux/issues/51
Signed-off-by: Tingmao Wang <m@maowtm.org>
---

Changes in v2:
- Factor out common code in hook_unix_stream_connect and
  hook_unix_may_send into check_socket_access(), and inline
  is_abstract_socket().

 security/landlock/audit.c |   4 ++
 security/landlock/audit.h |   1 +
 security/landlock/task.c  | 109 ++++++++++++++++++++++----------------
 3 files changed, 67 insertions(+), 47 deletions(-)

diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index e899995f1fd5..0626cc553ab0 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -75,6 +75,10 @@ get_blocker(const enum landlock_request_type type,
 		WARN_ON_ONCE(access_bit != -1);
 		return "scope.abstract_unix_socket";
 
+	case LANDLOCK_REQUEST_SCOPE_PATHNAME_UNIX_SOCKET:
+		WARN_ON_ONCE(access_bit != -1);
+		return "scope.pathname_unix_socket";
+
 	case LANDLOCK_REQUEST_SCOPE_SIGNAL:
 		WARN_ON_ONCE(access_bit != -1);
 		return "scope.signal";
diff --git a/security/landlock/audit.h b/security/landlock/audit.h
index 92428b7fc4d8..1c9ce8588102 100644
--- a/security/landlock/audit.h
+++ b/security/landlock/audit.h
@@ -21,6 +21,7 @@ enum landlock_request_type {
 	LANDLOCK_REQUEST_NET_ACCESS,
 	LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
 	LANDLOCK_REQUEST_SCOPE_SIGNAL,
+	LANDLOCK_REQUEST_SCOPE_PATHNAME_UNIX_SOCKET,
 };
 
 /*
diff --git a/security/landlock/task.c b/security/landlock/task.c
index 833bc0cfe5c9..10dc356baf6f 100644
--- a/security/landlock/task.c
+++ b/security/landlock/task.c
@@ -232,35 +232,81 @@ static bool domain_is_scoped(const struct landlock_ruleset *const client,
 	return false;
 }
 
+/**
+ * sock_is_scoped - Check if socket connect or send should be restricted
+ *    based on scope controls.
+ *
+ * @other: The server socket.
+ * @domain: The client domain.
+ * @scope: The relevant scope bit to check (i.e. pathname or abstract).
+ *
+ * Returns: True if connect should be restricted, false otherwise.
+ */
 static bool sock_is_scoped(struct sock *const other,
-			   const struct landlock_ruleset *const domain)
+			   const struct landlock_ruleset *const domain,
+			   access_mask_t scope)
 {
 	const struct landlock_ruleset *dom_other;
 
 	/* The credentials will not change. */
 	lockdep_assert_held(&unix_sk(other)->lock);
 	dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain;
-	return domain_is_scoped(domain, dom_other,
-				LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
+	return domain_is_scoped(domain, dom_other, scope);
 }
 
-static bool is_abstract_socket(struct sock *const sock)
+/* Allow us to quickly test if the current domain scopes any form of socket */
+static const struct access_masks unix_scope = {
+	.scope = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
+		 LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET,
+};
+
+/*
+ * UNIX sockets can have three types of addresses: pathname (a filesystem path),
+ * unnamed (not bound to an address), and abstract (sun_path[0] is '\0').
+ * Unnamed sockets include those created with socketpair() and unbound sockets.
+ * We do not restrict unnamed sockets since they have no address to identify.
+ */
+static int
+check_socket_access(struct sock *const other,
+		    const struct landlock_cred_security *const subject,
+		    const size_t handle_layer)
 {
-	struct unix_address *addr = unix_sk(sock)->addr;
+	const struct unix_address *addr = unix_sk(other)->addr;
+	access_mask_t scope;
+	enum landlock_request_type request_type;
 
+	/* Unnamed sockets are not restricted. */
 	if (!addr)
-		return false;
+		return 0;
 
+	/*
+	 * Abstract and pathname Unix sockets have separate scope and audit
+	 * request type.
+	 */
 	if (addr->len >= offsetof(struct sockaddr_un, sun_path) + 1 &&
-	    addr->name->sun_path[0] == '\0')
-		return true;
+	    addr->name->sun_path[0] == '\0') {
+		scope = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET;
+		request_type = LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET;
+	} else {
+		scope = LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
+		request_type = LANDLOCK_REQUEST_SCOPE_PATHNAME_UNIX_SOCKET;
+	}
 
-	return false;
-}
+	if (!sock_is_scoped(other, subject->domain, scope))
+		return 0;
 
-static const struct access_masks unix_scope = {
-	.scope = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
-};
+	landlock_log_denial(subject, &(struct landlock_request) {
+		.type = request_type,
+		.audit = {
+			.type = LSM_AUDIT_DATA_NET,
+			.u.net = &(struct lsm_network_audit) {
+				.sk = other,
+			},
+		},
+		.layer_plus_one = handle_layer + 1,
+	});
+	return -EPERM;
+}
 
 static int hook_unix_stream_connect(struct sock *const sock,
 				    struct sock *const other,
@@ -275,23 +321,7 @@ static int hook_unix_stream_connect(struct sock *const sock,
 	if (!subject)
 		return 0;
 
-	if (!is_abstract_socket(other))
-		return 0;
-
-	if (!sock_is_scoped(other, subject->domain))
-		return 0;
-
-	landlock_log_denial(subject, &(struct landlock_request) {
-		.type = LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
-		.audit = {
-			.type = LSM_AUDIT_DATA_NET,
-			.u.net = &(struct lsm_network_audit) {
-				.sk = other,
-			},
-		},
-		.layer_plus_one = handle_layer + 1,
-	});
-	return -EPERM;
+	return check_socket_access(other, subject, handle_layer);
 }
 
 static int hook_unix_may_send(struct socket *const sock,
@@ -302,6 +332,7 @@ static int hook_unix_may_send(struct socket *const sock,
 		landlock_get_applicable_subject(current_cred(), unix_scope,
 						&handle_layer);
 
+	/* Quick return for non-landlocked tasks. */
 	if (!subject)
 		return 0;
 
@@ -312,23 +343,7 @@ static int hook_unix_may_send(struct socket *const sock,
 	if (unix_peer(sock->sk) == other->sk)
 		return 0;
 
-	if (!is_abstract_socket(other->sk))
-		return 0;
-
-	if (!sock_is_scoped(other->sk, subject->domain))
-		return 0;
-
-	landlock_log_denial(subject, &(struct landlock_request) {
-		.type = LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
-		.audit = {
-			.type = LSM_AUDIT_DATA_NET,
-			.u.net = &(struct lsm_network_audit) {
-				.sk = other->sk,
-			},
-		},
-		.layer_plus_one = handle_layer + 1,
-	});
-	return -EPERM;
+	return check_socket_access(other->sk, subject, handle_layer);
 }
 
 static const struct access_masks signal_scope = {
-- 
2.52.0

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

* [PATCH v2 3/6] samples/landlock: Support LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
  2025-12-30 17:20 [PATCH v2 0/6] Landlock: Implement scope control for pathname Unix sockets Tingmao Wang
  2025-12-30 17:20 ` [PATCH v2 1/6] landlock: Add LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET scope bit to uAPI Tingmao Wang
  2025-12-30 17:20 ` [PATCH v2 2/6] landlock: Implement LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET Tingmao Wang
@ 2025-12-30 17:20 ` Tingmao Wang
  2025-12-30 17:20 ` [PATCH v2 4/6] selftests/landlock: Support pathname socket path in set_unix_address Tingmao Wang
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: Tingmao Wang @ 2025-12-30 17:20 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Tingmao Wang, Günther Noack, Demi Marie Obenour, Alyssa Ross,
	Jann Horn, Tahera Fahimi, Justin Suess, linux-security-module

Signed-off-by: Tingmao Wang <m@maowtm.org>
---

I've decided to use "u" as the character to control this scope bit since
it stands for (normal) Unix sockets.  Imo using "p" or "n" would make it less
clear / memorable.  Open to suggestions.

Also, open to suggestion whether socket scoping (pathname and abstract)
should be enabled by default, if LL_SCOPED is not set.  This would break
backward compatibility, but maybe we shouldn't guarentee backward
compatibility of this sandboxer in the first place, and almost all cases
of Landlock usage would want socket scoping.

 samples/landlock/sandboxer.c | 23 ++++++++++++++++++-----
 1 file changed, 18 insertions(+), 5 deletions(-)

diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
index e7af02f98208..2de14e1c787d 100644
--- a/samples/landlock/sandboxer.c
+++ b/samples/landlock/sandboxer.c
@@ -234,14 +234,16 @@ static bool check_ruleset_scope(const char *const env_var,
 	bool error = false;
 	bool abstract_scoping = false;
 	bool signal_scoping = false;
+	bool named_scoping = false;
 
 	/* Scoping is not supported by Landlock ABI */
 	if (!(ruleset_attr->scoped &
-	      (LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL)))
+	      (LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL |
+	       LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET)))
 		goto out_unset;
 
 	env_type_scope = getenv(env_var);
-	/* Scoping is not supported by the user */
+	/* Scoping is not requested by the user */
 	if (!env_type_scope || strcmp("", env_type_scope) == 0)
 		goto out_unset;
 
@@ -254,6 +256,9 @@ static bool check_ruleset_scope(const char *const env_var,
 		} else if (strcmp("s", ipc_scoping_name) == 0 &&
 			   !signal_scoping) {
 			signal_scoping = true;
+		} else if (strcmp("u", ipc_scoping_name) == 0 &&
+			   !named_scoping) {
+			named_scoping = true;
 		} else {
 			fprintf(stderr, "Unknown or duplicate scope \"%s\"\n",
 				ipc_scoping_name);
@@ -270,6 +275,8 @@ static bool check_ruleset_scope(const char *const env_var,
 		ruleset_attr->scoped &= ~LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET;
 	if (!signal_scoping)
 		ruleset_attr->scoped &= ~LANDLOCK_SCOPE_SIGNAL;
+	if (!named_scoping)
+		ruleset_attr->scoped &= ~LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
 
 	unsetenv(env_var);
 	return error;
@@ -299,7 +306,7 @@ static bool check_ruleset_scope(const char *const env_var,
 
 /* clang-format on */
 
-#define LANDLOCK_ABI_LAST 7
+#define LANDLOCK_ABI_LAST 8
 
 #define XSTR(s) #s
 #define STR(s) XSTR(s)
@@ -325,6 +332,7 @@ static const char help[] =
 	"* " ENV_SCOPED_NAME ": actions denied on the outside of the landlock domain\n"
 	"  - \"a\" to restrict opening abstract unix sockets\n"
 	"  - \"s\" to restrict sending signals\n"
+	"  - \"u\" to restrict opening pathname (non-abstract) unix sockets\n"
 	"\n"
 	"A sandboxer should not log denied access requests to avoid spamming logs, "
 	"but to test audit we can set " ENV_FORCE_LOG_NAME "=1\n"
@@ -334,7 +342,7 @@ static const char help[] =
 	ENV_FS_RW_NAME "=\"/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp\" "
 	ENV_TCP_BIND_NAME "=\"9418\" "
 	ENV_TCP_CONNECT_NAME "=\"80:443\" "
-	ENV_SCOPED_NAME "=\"a:s\" "
+	ENV_SCOPED_NAME "=\"a:s:u\" "
 	"%1$s bash -i\n"
 	"\n"
 	"This sandboxer can use Landlock features up to ABI version "
@@ -356,7 +364,8 @@ int main(const int argc, char *const argv[], char *const *const envp)
 		.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
 				      LANDLOCK_ACCESS_NET_CONNECT_TCP,
 		.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
-			  LANDLOCK_SCOPE_SIGNAL,
+			  LANDLOCK_SCOPE_SIGNAL |
+			  LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET,
 	};
 	int supported_restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
 	int set_restrict_flags = 0;
@@ -436,6 +445,10 @@ int main(const int argc, char *const argv[], char *const *const envp)
 		/* Removes LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON for ABI < 7 */
 		supported_restrict_flags &=
 			~LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
+		__attribute__((fallthrough));
+	case 7:
+		/* Removes LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET for ABI < 8 */
+		ruleset_attr.scoped &= ~LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
 
 		/* Must be printed for any ABI < LANDLOCK_ABI_LAST. */
 		fprintf(stderr,
-- 
2.52.0

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

* [PATCH v2 4/6] selftests/landlock: Support pathname socket path in set_unix_address
  2025-12-30 17:20 [PATCH v2 0/6] Landlock: Implement scope control for pathname Unix sockets Tingmao Wang
                   ` (2 preceding siblings ...)
  2025-12-30 17:20 ` [PATCH v2 3/6] samples/landlock: Support LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET Tingmao Wang
@ 2025-12-30 17:20 ` Tingmao Wang
  2025-12-30 17:20 ` [PATCH v2 5/6] selftests/landlock: Repurpose scoped_abstract_unix_test.c for pathname sockets too Tingmao Wang
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: Tingmao Wang @ 2025-12-30 17:20 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Tingmao Wang, Günther Noack, Demi Marie Obenour, Alyssa Ross,
	Jann Horn, Tahera Fahimi, Justin Suess, linux-security-module

To prepare for extending the socket tests to do non-abstract sockets too,
extend set_unix_address() to also be able to populate a non-abstract
socket path under TMP_DIR.  Also use snprintf for good measure.

This also changes existing callers to pass true for the abstract argument.

Signed-off-by: Tingmao Wang <m@maowtm.org>
---
 tools/testing/selftests/landlock/common.h     | 33 +++++++++++++++----
 tools/testing/selftests/landlock/net_test.c   |  2 +-
 .../landlock/scoped_abstract_unix_test.c      | 30 ++++++++---------
 .../selftests/landlock/scoped_signal_test.c   |  2 +-
 4 files changed, 44 insertions(+), 23 deletions(-)

diff --git a/tools/testing/selftests/landlock/common.h b/tools/testing/selftests/landlock/common.h
index 90551650299c..c55c11434e27 100644
--- a/tools/testing/selftests/landlock/common.h
+++ b/tools/testing/selftests/landlock/common.h
@@ -241,13 +241,34 @@ struct service_fixture {
 	};
 };
 
+#define PATHNAME_UNIX_SOCK_DIR TMP_DIR
+
+/**
+ * set_unix_address - Set up srv->unix_addr and srv->unix_addr_len.
+ * @srv: Service fixture containing the socket address to initialize
+ * @index: Index to include in socket names
+ * @abstract: If true, creates an abstract socket address (sun_path[0] ==
+ *     '\0') with the given name.  If false, creates a pathname socket
+ *     address with the given path.
+ */
 static void __maybe_unused set_unix_address(struct service_fixture *const srv,
-					    const unsigned short index)
+					    const unsigned short index,
+					    const bool abstract)
 {
 	srv->unix_addr.sun_family = AF_UNIX;
-	sprintf(srv->unix_addr.sun_path,
-		"_selftests-landlock-abstract-unix-tid%d-index%d", sys_gettid(),
-		index);
-	srv->unix_addr_len = SUN_LEN(&srv->unix_addr);
-	srv->unix_addr.sun_path[0] = '\0';
+	if (abstract) {
+		snprintf(srv->unix_addr.sun_path,
+			 sizeof(srv->unix_addr.sun_path),
+			 "_selftests-landlock-abstract-unix-tid%d-index%d",
+			 sys_gettid(), index);
+		srv->unix_addr_len = SUN_LEN(&srv->unix_addr);
+		srv->unix_addr.sun_path[0] = '\0';
+	} else {
+		snprintf(srv->unix_addr.sun_path,
+			 sizeof(srv->unix_addr.sun_path),
+			 PATHNAME_UNIX_SOCK_DIR
+			 "/pathname-unix-tid%d-index%d.sock",
+			 sys_gettid(), index);
+		srv->unix_addr_len = sizeof(srv->unix_addr);
+	}
 }
diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c
index b34b139b3f89..fd3fe51ce92f 100644
--- a/tools/testing/selftests/landlock/net_test.c
+++ b/tools/testing/selftests/landlock/net_test.c
@@ -69,7 +69,7 @@ static int set_service(struct service_fixture *const srv,
 		return 0;
 
 	case AF_UNIX:
-		set_unix_address(srv, index);
+		set_unix_address(srv, index, true);
 		return 0;
 	}
 	return 1;
diff --git a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c
index 72f97648d4a7..4a790e2d387d 100644
--- a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c
+++ b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c
@@ -58,8 +58,8 @@ FIXTURE_SETUP(scoped_domains)
 
 	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);
-	set_unix_address(&self->dgram_address, 1);
+	set_unix_address(&self->stream_address, 0, true);
+	set_unix_address(&self->dgram_address, 1, true);
 }
 
 FIXTURE_TEARDOWN(scoped_domains)
@@ -280,7 +280,7 @@ FIXTURE_SETUP(scoped_audit)
 	disable_caps(_metadata);
 
 	memset(&self->dgram_address, 0, sizeof(self->dgram_address));
-	set_unix_address(&self->dgram_address, 1);
+	set_unix_address(&self->dgram_address, 1, true);
 
 	set_cap(_metadata, CAP_AUDIT_CONTROL);
 	self->audit_fd = audit_init_with_exe_filter(&self->audit_filter);
@@ -392,16 +392,16 @@ FIXTURE_SETUP(scoped_vs_unscoped)
 
 	memset(&self->parent_stream_address, 0,
 	       sizeof(self->parent_stream_address));
-	set_unix_address(&self->parent_stream_address, 0);
+	set_unix_address(&self->parent_stream_address, 0, true);
 	memset(&self->parent_dgram_address, 0,
 	       sizeof(self->parent_dgram_address));
-	set_unix_address(&self->parent_dgram_address, 1);
+	set_unix_address(&self->parent_dgram_address, 1, true);
 	memset(&self->child_stream_address, 0,
 	       sizeof(self->child_stream_address));
-	set_unix_address(&self->child_stream_address, 2);
+	set_unix_address(&self->child_stream_address, 2, true);
 	memset(&self->child_dgram_address, 0,
 	       sizeof(self->child_dgram_address));
-	set_unix_address(&self->child_dgram_address, 3);
+	set_unix_address(&self->child_dgram_address, 3, true);
 }
 
 FIXTURE_TEARDOWN(scoped_vs_unscoped)
@@ -622,9 +622,9 @@ FIXTURE_SETUP(outside_socket)
 	drop_caps(_metadata);
 
 	memset(&self->transit_address, 0, sizeof(self->transit_address));
-	set_unix_address(&self->transit_address, 0);
+	set_unix_address(&self->transit_address, 0, true);
 	memset(&self->address, 0, sizeof(self->address));
-	set_unix_address(&self->address, 1);
+	set_unix_address(&self->address, 1, true);
 }
 
 FIXTURE_TEARDOWN(outside_socket)
@@ -802,9 +802,9 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)
 
 	/* Abstract address. */
 	memset(&stream_abstract_addr, 0, sizeof(stream_abstract_addr));
-	set_unix_address(&stream_abstract_addr, 0);
+	set_unix_address(&stream_abstract_addr, 0, true);
 	memset(&dgram_abstract_addr, 0, sizeof(dgram_abstract_addr));
-	set_unix_address(&dgram_abstract_addr, 1);
+	set_unix_address(&dgram_abstract_addr, 1, true);
 
 	/* Unnamed address for datagram socket. */
 	ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_DGRAM, 0, unnamed_sockets));
@@ -990,9 +990,9 @@ TEST(datagram_sockets)
 
 	drop_caps(_metadata);
 	memset(&connected_addr, 0, sizeof(connected_addr));
-	set_unix_address(&connected_addr, 0);
+	set_unix_address(&connected_addr, 0, true);
 	memset(&non_connected_addr, 0, sizeof(non_connected_addr));
-	set_unix_address(&non_connected_addr, 1);
+	set_unix_address(&non_connected_addr, 1, true);
 
 	ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
 	ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
@@ -1090,9 +1090,9 @@ TEST(self_connect)
 
 	drop_caps(_metadata);
 	memset(&connected_addr, 0, sizeof(connected_addr));
-	set_unix_address(&connected_addr, 0);
+	set_unix_address(&connected_addr, 0, true);
 	memset(&non_connected_addr, 0, sizeof(non_connected_addr));
-	set_unix_address(&non_connected_addr, 1);
+	set_unix_address(&non_connected_addr, 1, true);
 
 	connected_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
 	non_connected_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
diff --git a/tools/testing/selftests/landlock/scoped_signal_test.c b/tools/testing/selftests/landlock/scoped_signal_test.c
index d8bf33417619..8d1e1dc89c43 100644
--- a/tools/testing/selftests/landlock/scoped_signal_test.c
+++ b/tools/testing/selftests/landlock/scoped_signal_test.c
@@ -463,7 +463,7 @@ TEST_F(fown, sigurg_socket)
 	pid_t child;
 
 	memset(&server_address, 0, sizeof(server_address));
-	set_unix_address(&server_address, 0);
+	set_unix_address(&server_address, 0, true);
 
 	ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
 	ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
-- 
2.52.0


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

* [PATCH v2 5/6] selftests/landlock: Repurpose scoped_abstract_unix_test.c for pathname sockets too.
  2025-12-30 17:20 [PATCH v2 0/6] Landlock: Implement scope control for pathname Unix sockets Tingmao Wang
                   ` (3 preceding siblings ...)
  2025-12-30 17:20 ` [PATCH v2 4/6] selftests/landlock: Support pathname socket path in set_unix_address Tingmao Wang
@ 2025-12-30 17:20 ` Tingmao Wang
  2025-12-30 17:20 ` [PATCH v2 6/6] selftests/landlock: Add pathname socket variants for more tests Tingmao Wang
  2025-12-30 23:16 ` [PATCH v2 0/6] Landlock: Implement scope control for pathname Unix sockets Günther Noack
  6 siblings, 0 replies; 9+ messages in thread
From: Tingmao Wang @ 2025-12-30 17:20 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Tingmao Wang, Günther Noack, Demi Marie Obenour, Alyssa Ross,
	Jann Horn, Tahera Fahimi, Justin Suess, linux-security-module

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@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@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
+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;
+	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 =
+			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


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

* [PATCH v2 6/6] selftests/landlock: Add pathname socket variants for more tests
  2025-12-30 17:20 [PATCH v2 0/6] Landlock: Implement scope control for pathname Unix sockets Tingmao Wang
                   ` (4 preceding siblings ...)
  2025-12-30 17:20 ` [PATCH v2 5/6] selftests/landlock: Repurpose scoped_abstract_unix_test.c for pathname sockets too Tingmao Wang
@ 2025-12-30 17:20 ` Tingmao Wang
  2025-12-30 23:16 ` [PATCH v2 0/6] Landlock: Implement scope control for pathname Unix sockets Günther Noack
  6 siblings, 0 replies; 9+ messages in thread
From: Tingmao Wang @ 2025-12-30 17:20 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Tingmao Wang, Günther Noack, Demi Marie Obenour, Alyssa Ross,
	Jann Horn, Tahera Fahimi, Justin Suess, linux-security-module

While this produces a lot of change, it does allow us to "simultaneously"
test both abstract and pathname UNIX sockets with reletively little code
duplication, since they are really similar.

Tests touched: scoped_vs_unscoped, outside_socket,
various_address_sockets, datagram_sockets, self_connect.

Signed-off-by: Tingmao Wang <m@maowtm.org>
---
 .../selftests/landlock/scoped_unix_test.c     | 599 ++++++++++++------
 1 file changed, 395 insertions(+), 204 deletions(-)

diff --git a/tools/testing/selftests/landlock/scoped_unix_test.c b/tools/testing/selftests/landlock/scoped_unix_test.c
index 669418c97509..6d1541f77dbe 100644
--- a/tools/testing/selftests/landlock/scoped_unix_test.c
+++ b/tools/testing/selftests/landlock/scoped_unix_test.c
@@ -536,8 +536,12 @@ TEST_F(scoped_audit, connect_to_child)
 
 FIXTURE(scoped_vs_unscoped)
 {
-	struct service_fixture parent_stream_address, parent_dgram_address,
-		child_stream_address, child_dgram_address;
+	struct service_fixture parent_stream_address_abstract,
+		parent_dgram_address_abstract, child_stream_address_abstract,
+		child_dgram_address_abstract;
+	struct service_fixture parent_stream_address_pathname,
+		parent_dgram_address_pathname, child_stream_address_pathname,
+		child_dgram_address_pathname;
 };
 
 #include "scoped_multiple_domain_variants.h"
@@ -546,35 +550,75 @@ FIXTURE_SETUP(scoped_vs_unscoped)
 {
 	drop_caps(_metadata);
 
-	memset(&self->parent_stream_address, 0,
-	       sizeof(self->parent_stream_address));
-	set_unix_address(&self->parent_stream_address, 0, true);
-	memset(&self->parent_dgram_address, 0,
-	       sizeof(self->parent_dgram_address));
-	set_unix_address(&self->parent_dgram_address, 1, true);
-	memset(&self->child_stream_address, 0,
-	       sizeof(self->child_stream_address));
-	set_unix_address(&self->child_stream_address, 2, true);
-	memset(&self->child_dgram_address, 0,
-	       sizeof(self->child_dgram_address));
-	set_unix_address(&self->child_dgram_address, 3, true);
+	ASSERT_EQ(0, mkdir(PATHNAME_UNIX_SOCK_DIR, 0700));
+
+	/* Abstract addresses. */
+	memset(&self->parent_stream_address_abstract, 0,
+	       sizeof(self->parent_stream_address_abstract));
+	set_unix_address(&self->parent_stream_address_abstract, 0, true);
+	memset(&self->parent_dgram_address_abstract, 0,
+	       sizeof(self->parent_dgram_address_abstract));
+	set_unix_address(&self->parent_dgram_address_abstract, 1, true);
+	memset(&self->child_stream_address_abstract, 0,
+	       sizeof(self->child_stream_address_abstract));
+	set_unix_address(&self->child_stream_address_abstract, 2, true);
+	memset(&self->child_dgram_address_abstract, 0,
+	       sizeof(self->child_dgram_address_abstract));
+	set_unix_address(&self->child_dgram_address_abstract, 3, true);
+
+	/* Pathname addresses. */
+	memset(&self->parent_stream_address_pathname, 0,
+	       sizeof(self->parent_stream_address_pathname));
+	set_unix_address(&self->parent_stream_address_pathname, 4, false);
+	memset(&self->parent_dgram_address_pathname, 0,
+	       sizeof(self->parent_dgram_address_pathname));
+	set_unix_address(&self->parent_dgram_address_pathname, 5, false);
+	memset(&self->child_stream_address_pathname, 0,
+	       sizeof(self->child_stream_address_pathname));
+	set_unix_address(&self->child_stream_address_pathname, 6, false);
+	memset(&self->child_dgram_address_pathname, 0,
+	       sizeof(self->child_dgram_address_pathname));
+	set_unix_address(&self->child_dgram_address_pathname, 7, false);
 }
 
 FIXTURE_TEARDOWN(scoped_vs_unscoped)
 {
+	EXPECT_EQ(0, remove_path(self->parent_stream_address_pathname.unix_addr.sun_path));
+	EXPECT_EQ(0, remove_path(self->parent_dgram_address_pathname.unix_addr.sun_path));
+	EXPECT_EQ(0, remove_path(self->child_stream_address_pathname.unix_addr.sun_path));
+	EXPECT_EQ(0, remove_path(self->child_dgram_address_pathname.unix_addr.sun_path));
+	EXPECT_EQ(0, rmdir(PATHNAME_UNIX_SOCK_DIR));
 }
 
 /*
  * Test unix_stream_connect and unix_may_send for parent, child and
  * grand child processes when they can have scoped or non-scoped domains.
  */
-TEST_F(scoped_vs_unscoped, unix_scoping)
+static void test_scoped_vs_unscoped(
+	struct __test_metadata *const _metadata,
+	FIXTURE_DATA(scoped_vs_unscoped) * self,
+	const FIXTURE_VARIANT(scoped_vs_unscoped) * variant,
+	const bool abstract)
 {
 	pid_t child;
 	int status;
 	bool can_connect_to_parent, can_connect_to_child;
 	int pipe_parent[2];
 	int stream_server_parent, dgram_server_parent;
+	const __u16 scope = abstract ? LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET :
+				       LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
+	const struct service_fixture *parent_stream_address =
+		abstract ? &self->parent_stream_address_abstract :
+			   &self->parent_stream_address_pathname;
+	const struct service_fixture *parent_dgram_address =
+		abstract ? &self->parent_dgram_address_abstract :
+			   &self->parent_dgram_address_pathname;
+	const struct service_fixture *child_stream_address =
+		abstract ? &self->child_stream_address_abstract :
+			   &self->child_stream_address_pathname;
+	const struct service_fixture *child_dgram_address =
+		abstract ? &self->child_dgram_address_abstract :
+			   &self->child_dgram_address_pathname;
 
 	can_connect_to_child = (variant->domain_grand_child != SCOPE_SANDBOX);
 	can_connect_to_parent = (can_connect_to_child &&
@@ -585,8 +629,7 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
 	if (variant->domain_all == OTHER_SANDBOX)
 		create_fs_domain(_metadata);
 	else if (variant->domain_all == SCOPE_SANDBOX)
-		create_scoped_domain(_metadata,
-				     LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
+		create_scoped_domain(_metadata, scope);
 
 	child = fork();
 	ASSERT_LE(0, child);
@@ -600,8 +643,7 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
 		if (variant->domain_children == OTHER_SANDBOX)
 			create_fs_domain(_metadata);
 		else if (variant->domain_children == SCOPE_SANDBOX)
-			create_scoped_domain(
-				_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
+			create_scoped_domain(_metadata, scope);
 
 		grand_child = fork();
 		ASSERT_LE(0, grand_child);
@@ -616,9 +658,7 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
 			if (variant->domain_grand_child == OTHER_SANDBOX)
 				create_fs_domain(_metadata);
 			else if (variant->domain_grand_child == SCOPE_SANDBOX)
-				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);
@@ -626,15 +666,13 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
 			ASSERT_LE(0, dgram_client);
 
 			ASSERT_EQ(1, read(pipe_child[0], &buf, 1));
-			stream_err = connect(
-				stream_client,
-				&self->child_stream_address.unix_addr,
-				self->child_stream_address.unix_addr_len);
+			stream_err = connect(stream_client,
+					     &child_stream_address->unix_addr,
+					     child_stream_address->unix_addr_len);
 			stream_errno = errno;
-			dgram_err = connect(
-				dgram_client,
-				&self->child_dgram_address.unix_addr,
-				self->child_dgram_address.unix_addr_len);
+			dgram_err = connect(dgram_client,
+					    &child_dgram_address->unix_addr,
+					    child_dgram_address->unix_addr_len);
 			dgram_errno = errno;
 			if (can_connect_to_child) {
 				EXPECT_EQ(0, stream_err);
@@ -653,14 +691,12 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
 
 			ASSERT_EQ(1, read(pipe_parent[0], &buf, 1));
 			stream_err = connect(
-				stream_client,
-				&self->parent_stream_address.unix_addr,
-				self->parent_stream_address.unix_addr_len);
+				stream_client, &parent_stream_address->unix_addr,
+				parent_stream_address->unix_addr_len);
 			stream_errno = errno;
 			dgram_err = connect(
-				dgram_client,
-				&self->parent_dgram_address.unix_addr,
-				self->parent_dgram_address.unix_addr_len);
+				dgram_client, &parent_dgram_address->unix_addr,
+				parent_dgram_address->unix_addr_len);
 			dgram_errno = errno;
 			if (can_connect_to_parent) {
 				EXPECT_EQ(0, stream_err);
@@ -681,8 +717,7 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
 		if (variant->domain_child == OTHER_SANDBOX)
 			create_fs_domain(_metadata);
 		else if (variant->domain_child == SCOPE_SANDBOX)
-			create_scoped_domain(
-				_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
+			create_scoped_domain(_metadata, scope);
 
 		stream_server_child = socket(AF_UNIX, SOCK_STREAM, 0);
 		ASSERT_LE(0, stream_server_child);
@@ -690,11 +725,11 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
 		ASSERT_LE(0, dgram_server_child);
 
 		ASSERT_EQ(0, bind(stream_server_child,
-				  &self->child_stream_address.unix_addr,
-				  self->child_stream_address.unix_addr_len));
-		ASSERT_EQ(0, bind(dgram_server_child,
-				  &self->child_dgram_address.unix_addr,
-				  self->child_dgram_address.unix_addr_len));
+				  &child_stream_address->unix_addr,
+				  child_stream_address->unix_addr_len));
+		ASSERT_EQ(0,
+			  bind(dgram_server_child, &child_dgram_address->unix_addr,
+			       child_dgram_address->unix_addr_len));
 		ASSERT_EQ(0, listen(stream_server_child, backlog));
 
 		ASSERT_EQ(1, write(pipe_child[1], ".", 1));
@@ -708,19 +743,16 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
 	if (variant->domain_parent == OTHER_SANDBOX)
 		create_fs_domain(_metadata);
 	else if (variant->domain_parent == SCOPE_SANDBOX)
-		create_scoped_domain(_metadata,
-				     LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
+		create_scoped_domain(_metadata, scope);
 
 	stream_server_parent = socket(AF_UNIX, SOCK_STREAM, 0);
 	ASSERT_LE(0, stream_server_parent);
 	dgram_server_parent = socket(AF_UNIX, SOCK_DGRAM, 0);
 	ASSERT_LE(0, dgram_server_parent);
-	ASSERT_EQ(0, bind(stream_server_parent,
-			  &self->parent_stream_address.unix_addr,
-			  self->parent_stream_address.unix_addr_len));
-	ASSERT_EQ(0, bind(dgram_server_parent,
-			  &self->parent_dgram_address.unix_addr,
-			  self->parent_dgram_address.unix_addr_len));
+	ASSERT_EQ(0, bind(stream_server_parent, &parent_stream_address->unix_addr,
+			  parent_stream_address->unix_addr_len));
+	ASSERT_EQ(0, bind(dgram_server_parent, &parent_dgram_address->unix_addr,
+			  parent_dgram_address->unix_addr_len));
 
 	ASSERT_EQ(0, listen(stream_server_parent, backlog));
 
@@ -734,57 +766,119 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
 		_metadata->exit_code = KSFT_FAIL;
 }
 
+TEST_F(scoped_vs_unscoped, unix_scoping_abstract)
+{
+	test_scoped_vs_unscoped(_metadata, self, variant, true);
+}
+
+TEST_F(scoped_vs_unscoped, unix_scoping_pathname)
+{
+	test_scoped_vs_unscoped(_metadata, self, variant, false);
+}
+
 FIXTURE(outside_socket)
 {
-	struct service_fixture address, transit_address;
+	struct service_fixture address_abstract, transit_address_abstract;
+	struct service_fixture address_pathname, transit_address_pathname;
 };
 
 FIXTURE_VARIANT(outside_socket)
 {
 	const bool child_socket;
 	const int type;
+	const bool abstract;
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(outside_socket, abstract_allow_dgram_child) {
+	/* clang-format on */
+	.child_socket = true,
+	.type = SOCK_DGRAM,
+	.abstract = true,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(outside_socket, abstract_deny_dgram_server) {
+	/* clang-format on */
+	.child_socket = false,
+	.type = SOCK_DGRAM,
+	.abstract = true,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(outside_socket, abstract_allow_stream_child) {
+	/* clang-format on */
+	.child_socket = true,
+	.type = SOCK_STREAM,
+	.abstract = true,
 };
 
 /* clang-format off */
-FIXTURE_VARIANT_ADD(outside_socket, allow_dgram_child) {
+FIXTURE_VARIANT_ADD(outside_socket, abstract_deny_stream_server) {
+	/* clang-format on */
+	.child_socket = false,
+	.type = SOCK_STREAM,
+	.abstract = true,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(outside_socket, pathname_allow_dgram_child) {
 	/* clang-format on */
 	.child_socket = true,
 	.type = SOCK_DGRAM,
+	.abstract = false,
 };
 
 /* clang-format off */
-FIXTURE_VARIANT_ADD(outside_socket, deny_dgram_server) {
+FIXTURE_VARIANT_ADD(outside_socket, pathname_deny_dgram_server) {
 	/* clang-format on */
 	.child_socket = false,
 	.type = SOCK_DGRAM,
+	.abstract = false,
 };
 
 /* clang-format off */
-FIXTURE_VARIANT_ADD(outside_socket, allow_stream_child) {
+FIXTURE_VARIANT_ADD(outside_socket, pathname_allow_stream_child) {
 	/* clang-format on */
 	.child_socket = true,
 	.type = SOCK_STREAM,
+	.abstract = false,
 };
 
 /* clang-format off */
-FIXTURE_VARIANT_ADD(outside_socket, deny_stream_server) {
+FIXTURE_VARIANT_ADD(outside_socket, pathname_deny_stream_server) {
 	/* clang-format on */
 	.child_socket = false,
 	.type = SOCK_STREAM,
+	.abstract = false,
 };
 
 FIXTURE_SETUP(outside_socket)
 {
 	drop_caps(_metadata);
 
-	memset(&self->transit_address, 0, sizeof(self->transit_address));
-	set_unix_address(&self->transit_address, 0, true);
-	memset(&self->address, 0, sizeof(self->address));
-	set_unix_address(&self->address, 1, true);
+	ASSERT_EQ(0, mkdir(PATHNAME_UNIX_SOCK_DIR, 0700));
+
+	/* Abstract addresses. */
+	memset(&self->transit_address_abstract, 0,
+	       sizeof(self->transit_address_abstract));
+	set_unix_address(&self->transit_address_abstract, 0, true);
+	memset(&self->address_abstract, 0, sizeof(self->address_abstract));
+	set_unix_address(&self->address_abstract, 1, true);
+
+	/* Pathname addresses. */
+	memset(&self->transit_address_pathname, 0,
+	       sizeof(self->transit_address_pathname));
+	set_unix_address(&self->transit_address_pathname, 2, false);
+	memset(&self->address_pathname, 0, sizeof(self->address_pathname));
+	set_unix_address(&self->address_pathname, 3, false);
 }
 
 FIXTURE_TEARDOWN(outside_socket)
 {
+	EXPECT_EQ(0, remove_path(self->transit_address_pathname.unix_addr.sun_path));
+	EXPECT_EQ(0, remove_path(self->address_pathname.unix_addr.sun_path));
+	EXPECT_EQ(0, rmdir(PATHNAME_UNIX_SOCK_DIR));
 }
 
 /*
@@ -798,6 +892,15 @@ TEST_F(outside_socket, socket_with_different_domain)
 	int pipe_child[2], pipe_parent[2];
 	char buf_parent;
 	int server_socket;
+	const __u16 scope = variant->abstract ?
+				    LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET :
+				    LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
+	const struct service_fixture *transit_address =
+		variant->abstract ? &self->transit_address_abstract :
+				    &self->transit_address_pathname;
+	const struct service_fixture *address =
+		variant->abstract ? &self->address_abstract :
+				    &self->address_pathname;
 
 	ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
 	ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
@@ -812,8 +915,7 @@ TEST_F(outside_socket, socket_with_different_domain)
 		EXPECT_EQ(0, close(pipe_child[0]));
 
 		/* Client always has a domain. */
-		create_scoped_domain(_metadata,
-				     LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
+		create_scoped_domain(_metadata, scope);
 
 		if (variant->child_socket) {
 			int data_socket, passed_socket, stream_server;
@@ -823,8 +925,8 @@ TEST_F(outside_socket, socket_with_different_domain)
 			stream_server = socket(AF_UNIX, SOCK_STREAM, 0);
 			ASSERT_LE(0, stream_server);
 			ASSERT_EQ(0, bind(stream_server,
-					  &self->transit_address.unix_addr,
-					  self->transit_address.unix_addr_len));
+					  &transit_address->unix_addr,
+					  transit_address->unix_addr_len));
 			ASSERT_EQ(0, listen(stream_server, backlog));
 			ASSERT_EQ(1, write(pipe_child[1], ".", 1));
 			data_socket = accept(stream_server, NULL, NULL);
@@ -839,8 +941,8 @@ TEST_F(outside_socket, socket_with_different_domain)
 
 		/* Waits for parent signal for connection. */
 		ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
-		err = connect(client_socket, &self->address.unix_addr,
-			      self->address.unix_addr_len);
+		err = connect(client_socket, &address->unix_addr,
+			      address->unix_addr_len);
 		if (variant->child_socket) {
 			EXPECT_EQ(0, err);
 		} else {
@@ -859,9 +961,8 @@ TEST_F(outside_socket, socket_with_different_domain)
 
 		ASSERT_LE(0, client_child);
 		ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1));
-		ASSERT_EQ(0, connect(client_child,
-				     &self->transit_address.unix_addr,
-				     self->transit_address.unix_addr_len));
+		ASSERT_EQ(0, connect(client_child, &transit_address->unix_addr,
+				     transit_address->unix_addr_len));
 		server_socket = recv_fd(client_child);
 		EXPECT_EQ(0, close(client_child));
 	} else {
@@ -870,10 +971,10 @@ TEST_F(outside_socket, socket_with_different_domain)
 	ASSERT_LE(0, server_socket);
 
 	/* Server always has a domain. */
-	create_scoped_domain(_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
+	create_scoped_domain(_metadata, scope);
 
-	ASSERT_EQ(0, bind(server_socket, &self->address.unix_addr,
-			  self->address.unix_addr_len));
+	ASSERT_EQ(0,
+		  bind(server_socket, &address->unix_addr, address->unix_addr_len));
 	if (variant->type == SOCK_STREAM)
 		ASSERT_EQ(0, listen(server_socket, backlog));
 
@@ -888,52 +989,85 @@ TEST_F(outside_socket, socket_with_different_domain)
 		_metadata->exit_code = KSFT_FAIL;
 }
 
-static const char stream_path[] = TMP_DIR "/stream.sock";
-static const char dgram_path[] = TMP_DIR "/dgram.sock";
-
 /* clang-format off */
-FIXTURE(various_address_sockets) {};
+FIXTURE(various_address_sockets) {
+	struct service_fixture stream_pathname_addr, dgram_pathname_addr;
+	struct service_fixture stream_abstract_addr, dgram_abstract_addr;
+};
 /* clang-format on */
 
-FIXTURE_VARIANT(various_address_sockets)
-{
-	const int domain;
+/*
+ * Test all 4 combinations of abstract and pathname socket scope bits,
+ * plus a case with no Landlock domain at all.
+ */
+/* clang-format off */
+FIXTURE_VARIANT(various_address_sockets) {
+	/* clang-format on */
+	const __u16 scope_bits;
+	const bool no_sandbox;
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(various_address_sockets, scope_abstract) {
+	/* clang-format on */
+	.scope_bits = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(various_address_sockets, scope_pathname) {
+	/* clang-format on */
+	.scope_bits = LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET,
 };
 
 /* clang-format off */
-FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_scoped_domain) {
+FIXTURE_VARIANT_ADD(various_address_sockets, scope_both) {
 	/* clang-format on */
-	.domain = SCOPE_SANDBOX,
+	.scope_bits = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
+		      LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET,
 };
 
 /* clang-format off */
-FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_other_domain) {
+FIXTURE_VARIANT_ADD(various_address_sockets, scope_none) {
 	/* clang-format on */
-	.domain = OTHER_SANDBOX,
+	.scope_bits = 0,
 };
 
 /* clang-format off */
-FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_no_domain) {
+FIXTURE_VARIANT_ADD(various_address_sockets, no_domain) {
 	/* clang-format on */
-	.domain = NO_SANDBOX,
+	.no_sandbox = true,
 };
 
 FIXTURE_SETUP(various_address_sockets)
 {
 	drop_caps(_metadata);
 
-	umask(0077);
-	ASSERT_EQ(0, mkdir(TMP_DIR, 0700));
+	ASSERT_EQ(0, mkdir(PATHNAME_UNIX_SOCK_DIR, 0700));
+
+	memset(&self->stream_pathname_addr, 0, sizeof(self->stream_pathname_addr));
+	set_unix_address(&self->stream_pathname_addr, 0, false);
+	memset(&self->dgram_pathname_addr, 0, sizeof(self->dgram_pathname_addr));
+	set_unix_address(&self->dgram_pathname_addr, 1, false);
+
+	memset(&self->stream_abstract_addr, 0, sizeof(self->stream_abstract_addr));
+	set_unix_address(&self->stream_abstract_addr, 2, true);
+	memset(&self->dgram_abstract_addr, 0, sizeof(self->dgram_abstract_addr));
+	set_unix_address(&self->dgram_abstract_addr, 3, true);
 }
 
 FIXTURE_TEARDOWN(various_address_sockets)
 {
-	EXPECT_EQ(0, unlink(stream_path));
-	EXPECT_EQ(0, unlink(dgram_path));
-	EXPECT_EQ(0, rmdir(TMP_DIR));
+	EXPECT_EQ(0, remove_path(self->stream_pathname_addr.unix_addr.sun_path));
+	EXPECT_EQ(0, remove_path(self->dgram_pathname_addr.unix_addr.sun_path));
+	EXPECT_EQ(0, rmdir(PATHNAME_UNIX_SOCK_DIR));
 }
 
-TEST_F(various_address_sockets, scoped_pathname_sockets)
+/*
+ * Test interaction of various scope flags (controlled by variant->domain)
+ * with pathname and abstract sockets when connecting from a sandboxed
+ * child.
+ */
+TEST_F(various_address_sockets, scoped_sockets)
 {
 	pid_t child;
 	int status;
@@ -942,25 +1076,10 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)
 	int unnamed_sockets[2];
 	int stream_pathname_socket, dgram_pathname_socket,
 		stream_abstract_socket, dgram_abstract_socket, data_socket;
-	struct service_fixture stream_abstract_addr, dgram_abstract_addr;
-	struct sockaddr_un stream_pathname_addr = {
-		.sun_family = AF_UNIX,
-	};
-	struct sockaddr_un dgram_pathname_addr = {
-		.sun_family = AF_UNIX,
-	};
-
-	/* Pathname address. */
-	snprintf(stream_pathname_addr.sun_path,
-		 sizeof(stream_pathname_addr.sun_path), "%s", stream_path);
-	snprintf(dgram_pathname_addr.sun_path,
-		 sizeof(dgram_pathname_addr.sun_path), "%s", dgram_path);
-
-	/* Abstract address. */
-	memset(&stream_abstract_addr, 0, sizeof(stream_abstract_addr));
-	set_unix_address(&stream_abstract_addr, 0, true);
-	memset(&dgram_abstract_addr, 0, sizeof(dgram_abstract_addr));
-	set_unix_address(&dgram_abstract_addr, 1, true);
+	bool pathname_restricted =
+		(variant->scope_bits & LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET);
+	bool abstract_restricted =
+		(variant->scope_bits & LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
 
 	/* Unnamed address for datagram socket. */
 	ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_DGRAM, 0, unnamed_sockets));
@@ -975,82 +1094,103 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)
 		EXPECT_EQ(0, close(pipe_parent[1]));
 		EXPECT_EQ(0, close(unnamed_sockets[1]));
 
-		if (variant->domain == SCOPE_SANDBOX)
-			create_scoped_domain(
-				_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
-		else if (variant->domain == OTHER_SANDBOX)
+		/* Create domain based on variant. */
+		if (variant->scope_bits)
+			create_scoped_domain(_metadata, variant->scope_bits);
+		else if (!variant->no_sandbox)
 			create_fs_domain(_metadata);
 
 		/* Waits for parent to listen. */
 		ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
 		EXPECT_EQ(0, close(pipe_parent[0]));
 
-		/* Checks that we can send data through a datagram socket. */
+		/* Checks that we can send data through a unnamed socket. */
 		ASSERT_EQ(1, write(unnamed_sockets[0], "a", 1));
 		EXPECT_EQ(0, close(unnamed_sockets[0]));
 
 		/* Connects with pathname sockets. */
 		stream_pathname_socket = socket(AF_UNIX, SOCK_STREAM, 0);
 		ASSERT_LE(0, stream_pathname_socket);
-		ASSERT_EQ(0,
-			  connect(stream_pathname_socket, &stream_pathname_addr,
-				  sizeof(stream_pathname_addr)));
-		ASSERT_EQ(1, write(stream_pathname_socket, "b", 1));
+		err = connect(stream_pathname_socket,
+			      &self->stream_pathname_addr.unix_addr,
+			      self->stream_pathname_addr.unix_addr_len);
+		if (pathname_restricted) {
+			EXPECT_EQ(-1, err);
+			EXPECT_EQ(EPERM, errno);
+		} else {
+			EXPECT_EQ(0, err);
+			ASSERT_EQ(1, write(stream_pathname_socket, "b", 1));
+		}
 		EXPECT_EQ(0, close(stream_pathname_socket));
 
-		/* Sends without connection. */
+		/* Sends without connection (pathname). */
 		dgram_pathname_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
 		ASSERT_LE(0, dgram_pathname_socket);
 		err = sendto(dgram_pathname_socket, "c", 1, 0,
-			     &dgram_pathname_addr, sizeof(dgram_pathname_addr));
-		EXPECT_EQ(1, err);
+			     &self->dgram_pathname_addr.unix_addr,
+			     self->dgram_pathname_addr.unix_addr_len);
+		if (pathname_restricted) {
+			EXPECT_EQ(-1, err);
+			EXPECT_EQ(EPERM, errno);
+		} else {
+			EXPECT_EQ(1, err);
+		}
+
+		/* Sends with connection (pathname). */
+		err = connect(dgram_pathname_socket,
+			      &self->dgram_pathname_addr.unix_addr,
+			      self->dgram_pathname_addr.unix_addr_len);
+		if (pathname_restricted) {
+			EXPECT_EQ(-1, err);
+			EXPECT_EQ(EPERM, errno);
+		} else {
+			EXPECT_EQ(0, err);
+			ASSERT_EQ(1, write(dgram_pathname_socket, "d", 1));
+		}
 
-		/* Sends with connection. */
-		ASSERT_EQ(0,
-			  connect(dgram_pathname_socket, &dgram_pathname_addr,
-				  sizeof(dgram_pathname_addr)));
-		ASSERT_EQ(1, write(dgram_pathname_socket, "d", 1));
 		EXPECT_EQ(0, close(dgram_pathname_socket));
 
 		/* Connects with abstract sockets. */
 		stream_abstract_socket = socket(AF_UNIX, SOCK_STREAM, 0);
 		ASSERT_LE(0, stream_abstract_socket);
 		err = connect(stream_abstract_socket,
-			      &stream_abstract_addr.unix_addr,
-			      stream_abstract_addr.unix_addr_len);
-		if (variant->domain == SCOPE_SANDBOX) {
+			      &self->stream_abstract_addr.unix_addr,
+			      self->stream_abstract_addr.unix_addr_len);
+		if (abstract_restricted) {
 			EXPECT_EQ(-1, err);
 			EXPECT_EQ(EPERM, errno);
 		} else {
 			EXPECT_EQ(0, err);
 			ASSERT_EQ(1, write(stream_abstract_socket, "e", 1));
 		}
+
 		EXPECT_EQ(0, close(stream_abstract_socket));
 
-		/* Sends without connection. */
+		/* Sends without connection (abstract). */
 		dgram_abstract_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
 		ASSERT_LE(0, dgram_abstract_socket);
 		err = sendto(dgram_abstract_socket, "f", 1, 0,
-			     &dgram_abstract_addr.unix_addr,
-			     dgram_abstract_addr.unix_addr_len);
-		if (variant->domain == SCOPE_SANDBOX) {
+			     &self->dgram_abstract_addr.unix_addr,
+			     self->dgram_abstract_addr.unix_addr_len);
+		if (abstract_restricted) {
 			EXPECT_EQ(-1, err);
 			EXPECT_EQ(EPERM, errno);
 		} else {
 			EXPECT_EQ(1, err);
 		}
 
-		/* Sends with connection. */
+		/* Sends with connection (abstract). */
 		err = connect(dgram_abstract_socket,
-			      &dgram_abstract_addr.unix_addr,
-			      dgram_abstract_addr.unix_addr_len);
-		if (variant->domain == SCOPE_SANDBOX) {
+			      &self->dgram_abstract_addr.unix_addr,
+			      self->dgram_abstract_addr.unix_addr_len);
+		if (abstract_restricted) {
 			EXPECT_EQ(-1, err);
 			EXPECT_EQ(EPERM, errno);
 		} else {
 			EXPECT_EQ(0, err);
 			ASSERT_EQ(1, write(dgram_abstract_socket, "g", 1));
 		}
+
 		EXPECT_EQ(0, close(dgram_abstract_socket));
 
 		_exit(_metadata->exit_code);
@@ -1062,27 +1202,30 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)
 	/* Sets up pathname servers. */
 	stream_pathname_socket = socket(AF_UNIX, SOCK_STREAM, 0);
 	ASSERT_LE(0, stream_pathname_socket);
-	ASSERT_EQ(0, bind(stream_pathname_socket, &stream_pathname_addr,
-			  sizeof(stream_pathname_addr)));
+	ASSERT_EQ(0, bind(stream_pathname_socket,
+			  &self->stream_pathname_addr.unix_addr,
+			  self->stream_pathname_addr.unix_addr_len));
 	ASSERT_EQ(0, listen(stream_pathname_socket, backlog));
 
 	dgram_pathname_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
 	ASSERT_LE(0, dgram_pathname_socket);
-	ASSERT_EQ(0, bind(dgram_pathname_socket, &dgram_pathname_addr,
-			  sizeof(dgram_pathname_addr)));
+	ASSERT_EQ(0, bind(dgram_pathname_socket,
+			  &self->dgram_pathname_addr.unix_addr,
+			  self->dgram_pathname_addr.unix_addr_len));
 
 	/* Sets up abstract servers. */
 	stream_abstract_socket = socket(AF_UNIX, SOCK_STREAM, 0);
 	ASSERT_LE(0, stream_abstract_socket);
-	ASSERT_EQ(0,
-		  bind(stream_abstract_socket, &stream_abstract_addr.unix_addr,
-		       stream_abstract_addr.unix_addr_len));
+	ASSERT_EQ(0, bind(stream_abstract_socket,
+			  &self->stream_abstract_addr.unix_addr,
+			  self->stream_abstract_addr.unix_addr_len));
+	ASSERT_EQ(0, listen(stream_abstract_socket, backlog));
 
 	dgram_abstract_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
 	ASSERT_LE(0, dgram_abstract_socket);
-	ASSERT_EQ(0, bind(dgram_abstract_socket, &dgram_abstract_addr.unix_addr,
-			  dgram_abstract_addr.unix_addr_len));
-	ASSERT_EQ(0, listen(stream_abstract_socket, backlog));
+	ASSERT_EQ(0, bind(dgram_abstract_socket,
+			  &self->dgram_abstract_addr.unix_addr,
+			  self->dgram_abstract_addr.unix_addr_len));
 
 	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
 	EXPECT_EQ(0, close(pipe_parent[1]));
@@ -1092,24 +1235,31 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)
 	ASSERT_EQ('a', buf_parent);
 	EXPECT_LE(0, close(unnamed_sockets[1]));
 
-	/* Reads from pathname sockets. */
-	data_socket = accept(stream_pathname_socket, NULL, NULL);
-	ASSERT_LE(0, data_socket);
-	ASSERT_EQ(1, read(data_socket, &buf_parent, sizeof(buf_parent)));
-	ASSERT_EQ('b', buf_parent);
-	EXPECT_EQ(0, close(data_socket));
-	EXPECT_EQ(0, close(stream_pathname_socket));
+	if (!pathname_restricted) {
+		/*
+		 * Reads from pathname sockets if we expect child to be able to
+		 * send.
+		 */
+		data_socket = accept(stream_pathname_socket, NULL, NULL);
+		ASSERT_LE(0, data_socket);
+		ASSERT_EQ(1,
+			  read(data_socket, &buf_parent, sizeof(buf_parent)));
+		ASSERT_EQ('b', buf_parent);
+		EXPECT_EQ(0, close(data_socket));
 
-	ASSERT_EQ(1,
-		  read(dgram_pathname_socket, &buf_parent, sizeof(buf_parent)));
-	ASSERT_EQ('c', buf_parent);
-	ASSERT_EQ(1,
-		  read(dgram_pathname_socket, &buf_parent, sizeof(buf_parent)));
-	ASSERT_EQ('d', buf_parent);
-	EXPECT_EQ(0, close(dgram_pathname_socket));
+		ASSERT_EQ(1, read(dgram_pathname_socket, &buf_parent,
+				  sizeof(buf_parent)));
+		ASSERT_EQ('c', buf_parent);
+		ASSERT_EQ(1, read(dgram_pathname_socket, &buf_parent,
+				  sizeof(buf_parent)));
+		ASSERT_EQ('d', buf_parent);
+	}
 
-	if (variant->domain != SCOPE_SANDBOX) {
-		/* Reads from abstract sockets if allowed to send. */
+	if (!abstract_restricted) {
+		/*
+		 * Reads from abstract sockets if we expect child to be able to
+		 * send.
+		 */
 		data_socket = accept(stream_abstract_socket, NULL, NULL);
 		ASSERT_LE(0, data_socket);
 		ASSERT_EQ(1,
@@ -1125,30 +1275,73 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)
 		ASSERT_EQ('g', buf_parent);
 	}
 
-	/* Waits for all abstract socket tests. */
+	/* Waits for child to complete, and only close the socket afterwards. */
 	ASSERT_EQ(child, waitpid(child, &status, 0));
 	EXPECT_EQ(0, close(stream_abstract_socket));
 	EXPECT_EQ(0, close(dgram_abstract_socket));
+	EXPECT_EQ(0, close(stream_pathname_socket));
+	EXPECT_EQ(0, close(dgram_pathname_socket));
 
 	if (WIFSIGNALED(status) || !WIFEXITED(status) ||
 	    WEXITSTATUS(status) != EXIT_SUCCESS)
 		_metadata->exit_code = KSFT_FAIL;
 }
 
-TEST(datagram_sockets)
+/* Fixture for datagram_sockets and self_connect tests */
+FIXTURE(socket_type_test)
 {
 	struct service_fixture connected_addr, non_connected_addr;
+};
+
+FIXTURE_VARIANT(socket_type_test)
+{
+	const bool abstract;
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(socket_type_test, abstract) {
+	/* clang-format on */
+	.abstract = true,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(socket_type_test, pathname) {
+	/* clang-format on */
+	.abstract = false,
+};
+
+FIXTURE_SETUP(socket_type_test)
+{
+	drop_caps(_metadata);
+
+	if (!variant->abstract)
+		ASSERT_EQ(0, mkdir(PATHNAME_UNIX_SOCK_DIR, 0700));
+
+	memset(&self->connected_addr, 0, sizeof(self->connected_addr));
+	set_unix_address(&self->connected_addr, 0, variant->abstract);
+	memset(&self->non_connected_addr, 0, sizeof(self->non_connected_addr));
+	set_unix_address(&self->non_connected_addr, 1, variant->abstract);
+}
+
+FIXTURE_TEARDOWN(socket_type_test)
+{
+	if (!variant->abstract) {
+		EXPECT_EQ(0, remove_path(self->connected_addr.unix_addr.sun_path));
+		EXPECT_EQ(0, remove_path(self->non_connected_addr.unix_addr.sun_path));
+		EXPECT_EQ(0, rmdir(PATHNAME_UNIX_SOCK_DIR));
+	}
+}
+
+TEST_F(socket_type_test, datagram_sockets)
+{
 	int server_conn_socket, server_unconn_socket;
 	int pipe_parent[2], pipe_child[2];
 	int status;
 	char buf;
 	pid_t child;
-
-	drop_caps(_metadata);
-	memset(&connected_addr, 0, sizeof(connected_addr));
-	set_unix_address(&connected_addr, 0, true);
-	memset(&non_connected_addr, 0, sizeof(non_connected_addr));
-	set_unix_address(&non_connected_addr, 1, true);
+	const __u16 scope = variant->abstract ?
+				    LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET :
+				    LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
 
 	ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
 	ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
@@ -1169,8 +1362,9 @@ TEST(datagram_sockets)
 		/* Waits for parent to listen. */
 		ASSERT_EQ(1, read(pipe_parent[0], &buf, 1));
 		ASSERT_EQ(0,
-			  connect(client_conn_socket, &connected_addr.unix_addr,
-				  connected_addr.unix_addr_len));
+			  connect(client_conn_socket,
+				  &self->connected_addr.unix_addr,
+				  self->connected_addr.unix_addr_len));
 
 		/*
 		 * Both connected and non-connected sockets can send data when
@@ -1178,13 +1372,12 @@ TEST(datagram_sockets)
 		 */
 		ASSERT_EQ(1, send(client_conn_socket, ".", 1, 0));
 		ASSERT_EQ(1, sendto(client_unconn_socket, ".", 1, 0,
-				    &non_connected_addr.unix_addr,
-				    non_connected_addr.unix_addr_len));
+				    &self->non_connected_addr.unix_addr,
+				    self->non_connected_addr.unix_addr_len));
 		ASSERT_EQ(1, write(pipe_child[1], ".", 1));
 
 		/* Scopes the domain. */
-		create_scoped_domain(_metadata,
-				     LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
+		create_scoped_domain(_metadata, scope);
 
 		/*
 		 * Connected socket sends data to the receiver, but the
@@ -1192,8 +1385,8 @@ TEST(datagram_sockets)
 		 */
 		ASSERT_EQ(1, send(client_conn_socket, ".", 1, 0));
 		ASSERT_EQ(-1, sendto(client_unconn_socket, ".", 1, 0,
-				     &non_connected_addr.unix_addr,
-				     non_connected_addr.unix_addr_len));
+				     &self->non_connected_addr.unix_addr,
+				     self->non_connected_addr.unix_addr_len));
 		ASSERT_EQ(EPERM, errno);
 		ASSERT_EQ(1, write(pipe_child[1], ".", 1));
 
@@ -1210,10 +1403,11 @@ TEST(datagram_sockets)
 	ASSERT_LE(0, server_conn_socket);
 	ASSERT_LE(0, server_unconn_socket);
 
-	ASSERT_EQ(0, bind(server_conn_socket, &connected_addr.unix_addr,
-			  connected_addr.unix_addr_len));
-	ASSERT_EQ(0, bind(server_unconn_socket, &non_connected_addr.unix_addr,
-			  non_connected_addr.unix_addr_len));
+	ASSERT_EQ(0, bind(server_conn_socket, &self->connected_addr.unix_addr,
+			  self->connected_addr.unix_addr_len));
+	ASSERT_EQ(0, bind(server_unconn_socket,
+			  &self->non_connected_addr.unix_addr,
+			  self->non_connected_addr.unix_addr_len));
 	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
 
 	/* Waits for child to test. */
@@ -1238,52 +1432,49 @@ TEST(datagram_sockets)
 		_metadata->exit_code = KSFT_FAIL;
 }
 
-TEST(self_connect)
+TEST_F(socket_type_test, self_connect)
 {
-	struct service_fixture connected_addr, non_connected_addr;
 	int connected_socket, non_connected_socket, status;
 	pid_t child;
-
-	drop_caps(_metadata);
-	memset(&connected_addr, 0, sizeof(connected_addr));
-	set_unix_address(&connected_addr, 0, true);
-	memset(&non_connected_addr, 0, sizeof(non_connected_addr));
-	set_unix_address(&non_connected_addr, 1, true);
+	const __u16 scope = variant->abstract ?
+				    LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET :
+				    LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
 
 	connected_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
 	non_connected_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
 	ASSERT_LE(0, connected_socket);
 	ASSERT_LE(0, non_connected_socket);
 
-	ASSERT_EQ(0, bind(connected_socket, &connected_addr.unix_addr,
-			  connected_addr.unix_addr_len));
-	ASSERT_EQ(0, bind(non_connected_socket, &non_connected_addr.unix_addr,
-			  non_connected_addr.unix_addr_len));
+	ASSERT_EQ(0, bind(connected_socket, &self->connected_addr.unix_addr,
+			  self->connected_addr.unix_addr_len));
+	ASSERT_EQ(0, bind(non_connected_socket,
+			  &self->non_connected_addr.unix_addr,
+			  self->non_connected_addr.unix_addr_len));
 
 	child = fork();
 	ASSERT_LE(0, child);
 	if (child == 0) {
 		/* Child's domain is scoped. */
-		create_scoped_domain(_metadata,
-				     LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
+		create_scoped_domain(_metadata, scope);
 
 		/*
 		 * The child inherits the sockets, and cannot connect or
 		 * send data to them.
 		 */
 		ASSERT_EQ(-1,
-			  connect(connected_socket, &connected_addr.unix_addr,
-				  connected_addr.unix_addr_len));
+			  connect(connected_socket,
+				  &self->connected_addr.unix_addr,
+				  self->connected_addr.unix_addr_len));
 		ASSERT_EQ(EPERM, errno);
 
 		ASSERT_EQ(-1, sendto(connected_socket, ".", 1, 0,
-				     &connected_addr.unix_addr,
-				     connected_addr.unix_addr_len));
+				     &self->connected_addr.unix_addr,
+				     self->connected_addr.unix_addr_len));
 		ASSERT_EQ(EPERM, errno);
 
 		ASSERT_EQ(-1, sendto(non_connected_socket, ".", 1, 0,
-				     &non_connected_addr.unix_addr,
-				     non_connected_addr.unix_addr_len));
+				     &self->non_connected_addr.unix_addr,
+				     self->non_connected_addr.unix_addr_len));
 		ASSERT_EQ(EPERM, errno);
 
 		EXPECT_EQ(0, close(connected_socket));
-- 
2.52.0


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

* Re: [PATCH v2 0/6] Landlock: Implement scope control for pathname Unix sockets
  2025-12-30 17:20 [PATCH v2 0/6] Landlock: Implement scope control for pathname Unix sockets Tingmao Wang
                   ` (5 preceding siblings ...)
  2025-12-30 17:20 ` [PATCH v2 6/6] selftests/landlock: Add pathname socket variants for more tests Tingmao Wang
@ 2025-12-30 23:16 ` Günther Noack
  2025-12-31 16:54   ` Demi Marie Obenour
  6 siblings, 1 reply; 9+ messages in thread
From: Günther Noack @ 2025-12-30 23:16 UTC (permalink / raw)
  To: Tingmao Wang
  Cc: Mickaël Salaün, Günther Noack, Demi Marie Obenour,
	Alyssa Ross, Jann Horn, Tahera Fahimi, Justin Suess,
	linux-security-module

Hello!

Thanks for sending this patch!

On Tue, Dec 30, 2025 at 05:20:18PM +0000, Tingmao Wang wrote:
> Changes in v2:
>   Fix grammar in doc, rebased on mic/next, and extracted common code from
>   hook_unix_stream_connect and hook_unix_may_send into a separate
>   function.
> 
> The rest is the same as the v1 cover letter:
> 
> This patch series extend the existing abstract Unix socket scoping to
> pathname (i.e. normal file-based) sockets as well, by adding a new scope
> bit LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET that works the same as
> LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, except that restricts pathname Unix
> sockets.  This means that a sandboxed process with this scope enabled will
> not be able to connect to Unix sockets created outside the sandbox via the
> filesystem.
> 
> There is a future plan [1] for allowing specific sockets based on FS
> hierarchy, but this series is only determining access based on domain
> parent-child relationship.  There is currently no way to allow specific
> (outside the Landlock domain) Unix sockets, and none of the existing
> Landlock filesystem controls apply to socket connect().
> 
> With this series, we can now properly protect against things like the the
> following while only relying on Landlock:
> 
>     (running under tmux)
>     root@6-19-0-rc1-dev-00023-g68f0b276cbeb ~# LL_FS_RO=/ LL_FS_RW= ./sandboxer bash
>     Executing the sandboxed command...
>     root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi
>     cat: /tmp/hi: No such file or directory
>     root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# tmux new-window 'echo hi > /tmp/hi'
>     root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi
>     hi
> 
> The above but with Unix socket scoping enabled (both pathname and abstract
> sockets) - the sandboxed shell can now no longer talk to tmux due to the
> socket being created from outside the Landlock sandbox:
> 
>     (running under tmux)
>     root@6-19-0-rc1-dev-00023-g68f0b276cbeb ~# LL_FS_RO=/ LL_FS_RW= LL_SCOPED=u:a ./sandboxer bash
>     Executing the sandboxed command...
>     root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi
>     cat: /tmp/hi: No such file or directory
>     root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# tmux new-window 'echo hi > /tmp/hi'
>     error connecting to /tmp/tmux-0/default (Operation not permitted)
>     root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi
>     cat: /tmp/hi: No such file or directory
> 
> Tmux is just one example.  In a standard systemd session, `systemd-run
> --user` can also be used (--user will run the command in the user's
> session, without requiring any root privileges), and likely a lot more if
> running in a desktop environment with many popular applications.  This
> change therefore makes it possible to create sandboxes without relying on
> additional mechanisms like seccomp to protect against such issues.
> 
> These kind of issues was originally discussed on here (I took the idea for
> systemd-run from Demi):
> https://spectrum-os.org/lists/archives/spectrum-devel/00256266-26db-40cf-8f5b-f7c7064084c2@gmail.com/

What is unclear to me from the examples and the description is: Why is
the boundary between allowed and denied connection targets drawn at
the border of the Landlock domain (the "scope") and why don't we solve
this with the file-system-based approach described in [1]?

**Do we have existing use cases where a service is both offered and
connected to all from within the same Landlock domain, and where the
process enforcing the policy does not control the child process enough
so that it would be possible to allow-list it with a
LANDLOCK_ACCESS_FS_CONNECT_UNIX rule?**

If we do not have such a use case, it seems that the planned FS-based
control mechanism from [1] would do the same job?  Long term, we might
be better off if we only have only one control -- as we have discussed
in [2], having two of these might mean that they interact in
unconventional and possibly confusing ways.

Apart from that, there are some other weaker hints that make me
slightly critical of this patch set:

* We discussed the idea that a FS-based path_beneath rule would act
  implicitly also as an exception for
  LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET, in [2] - one possible way to
  interpret this is that the gravity of the system's logic pulls us
  back towards a FS-based control, and we would have to swim less
  against the stream if we integrated the Unix connect() control in
  that way?

* I am struggling to convince myself that we can tell users to
  restrict LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET as a default, as we are
  currently doing it with the other "scoped" and file system controls.
  (The scoped signals are OK because killing out-of-domain processes
  is clearly bad.  The scoped abstract sockets are usually OK because
  most processes do not need that feature.)

  But there are legitimate Unix services that are still needed by
  unprivileged processes and which are designed to be safe to use.
  For instance, systemd exposes a user database lookup service over
  varlink [3], which can be accessed from arbitrary glibc programs
  through a NSS module.  Using this is incompatible with
  LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET as long as we do not have the
  FS-based control and the surprising implicit permission through a
  path_beneath rule as discussed in [2].

  (Another example is the X11 socket, to which the same reasoning
  should apply, I think, and which is also used by a large number of
  programs.)

I agree that the bug [1] has been asleep for a bit too long, and we
should probably pick this up soon.  As we have not heard back from
Ryan after our last inquiry, IMHO I think it would be fair to take it
over.

Apologies for the difficult feedback - I do not mean to get in the way
of your enthusiasm here, but I would like to make sure that we don't
implement this as a stop-gap measure just because the other bug [1]
seemed more difficult and/or stuck in a github issue interaction.
Let's rather unblock that bug, if that is the case. :)

As usual, I fully acknowledge that I might well be wrong and might
have missed some of the underlying reasons, in which case I will
happily be corrected and change my mind. :)

[1] https://github.com/landlock-lsm/linux/issues/36
[2] https://github.com/landlock-lsm/linux/issues/36#issuecomment-3699749541
[3] https://systemd.io/USER_GROUP_API/

Have a good start into the new year!
–Günther


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

* Re: [PATCH v2 0/6] Landlock: Implement scope control for pathname Unix sockets
  2025-12-30 23:16 ` [PATCH v2 0/6] Landlock: Implement scope control for pathname Unix sockets Günther Noack
@ 2025-12-31 16:54   ` Demi Marie Obenour
  0 siblings, 0 replies; 9+ messages in thread
From: Demi Marie Obenour @ 2025-12-31 16:54 UTC (permalink / raw)
  To: Günther Noack, Tingmao Wang
  Cc: Mickaël Salaün, Günther Noack, Alyssa Ross,
	Jann Horn, Tahera Fahimi, Justin Suess, linux-security-module


[-- Attachment #1.1.1: Type: text/plain, Size: 7681 bytes --]

On 12/30/25 18:16, Günther Noack wrote:
> Hello!
> 
> Thanks for sending this patch!
> 
> On Tue, Dec 30, 2025 at 05:20:18PM +0000, Tingmao Wang wrote:
>> Changes in v2:
>>   Fix grammar in doc, rebased on mic/next, and extracted common code from
>>   hook_unix_stream_connect and hook_unix_may_send into a separate
>>   function.
>>
>> The rest is the same as the v1 cover letter:
>>
>> This patch series extend the existing abstract Unix socket scoping to
>> pathname (i.e. normal file-based) sockets as well, by adding a new scope
>> bit LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET that works the same as
>> LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, except that restricts pathname Unix
>> sockets.  This means that a sandboxed process with this scope enabled will
>> not be able to connect to Unix sockets created outside the sandbox via the
>> filesystem.
>>
>> There is a future plan [1] for allowing specific sockets based on FS
>> hierarchy, but this series is only determining access based on domain
>> parent-child relationship.  There is currently no way to allow specific
>> (outside the Landlock domain) Unix sockets, and none of the existing
>> Landlock filesystem controls apply to socket connect().
>>
>> With this series, we can now properly protect against things like the the
>> following while only relying on Landlock:
>>
>>     (running under tmux)
>>     root@6-19-0-rc1-dev-00023-g68f0b276cbeb ~# LL_FS_RO=/ LL_FS_RW= ./sandboxer bash
>>     Executing the sandboxed command...
>>     root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi
>>     cat: /tmp/hi: No such file or directory
>>     root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# tmux new-window 'echo hi > /tmp/hi'
>>     root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi
>>     hi
>>
>> The above but with Unix socket scoping enabled (both pathname and abstract
>> sockets) - the sandboxed shell can now no longer talk to tmux due to the
>> socket being created from outside the Landlock sandbox:
>>
>>     (running under tmux)
>>     root@6-19-0-rc1-dev-00023-g68f0b276cbeb ~# LL_FS_RO=/ LL_FS_RW= LL_SCOPED=u:a ./sandboxer bash
>>     Executing the sandboxed command...
>>     root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi
>>     cat: /tmp/hi: No such file or directory
>>     root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# tmux new-window 'echo hi > /tmp/hi'
>>     error connecting to /tmp/tmux-0/default (Operation not permitted)
>>     root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi
>>     cat: /tmp/hi: No such file or directory
>>
>> Tmux is just one example.  In a standard systemd session, `systemd-run
>> --user` can also be used (--user will run the command in the user's
>> session, without requiring any root privileges), and likely a lot more if
>> running in a desktop environment with many popular applications.  This
>> change therefore makes it possible to create sandboxes without relying on
>> additional mechanisms like seccomp to protect against such issues.
>>
>> These kind of issues was originally discussed on here (I took the idea for
>> systemd-run from Demi):
>> https://spectrum-os.org/lists/archives/spectrum-devel/00256266-26db-40cf-8f5b-f7c7064084c2@gmail.com/
> 
> What is unclear to me from the examples and the description is: Why is
> the boundary between allowed and denied connection targets drawn at
> the border of the Landlock domain (the "scope") and why don't we solve
> this with the file-system-based approach described in [1]?
> 
> **Do we have existing use cases where a service is both offered and
> connected to all from within the same Landlock domain, and where the
> process enforcing the policy does not control the child process enough
> so that it would be possible to allow-list it with a
> LANDLOCK_ACCESS_FS_CONNECT_UNIX rule?**
> 
> If we do not have such a use case, it seems that the planned FS-based
> control mechanism from [1] would do the same job?  Long term, we might
> be better off if we only have only one control -- as we have discussed
> in [2], having two of these might mean that they interact in
> unconventional and possibly confusing ways.

I agree with this.

> Apart from that, there are some other weaker hints that make me
> slightly critical of this patch set:
> 
> * We discussed the idea that a FS-based path_beneath rule would act
>   implicitly also as an exception for
>   LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET, in [2] - one possible way to
>   interpret this is that the gravity of the system's logic pulls us
>   back towards a FS-based control, and we would have to swim less
>   against the stream if we integrated the Unix connect() control in
>   that way?

I agree with this as well.  Having FS-based controls apply consistently
to all types of inodes is what I would expect.

> * I am struggling to convince myself that we can tell users to
>   restrict LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET as a default, as we are
>   currently doing it with the other "scoped" and file system controls.
>   (The scoped signals are OK because killing out-of-domain processes
>   is clearly bad.  The scoped abstract sockets are usually OK because
>   most processes do not need that feature.)
> 
>   But there are legitimate Unix services that are still needed by
>   unprivileged processes and which are designed to be safe to use.
>   For instance, systemd exposes a user database lookup service over
>   varlink [3], which can be accessed from arbitrary glibc programs
>   through a NSS module.  Using this is incompatible with
>   LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET as long as we do not have the
>   FS-based control and the surprising implicit permission through a
>   path_beneath rule as discussed in [2].
> 
>   (Another example is the X11 socket, to which the same reasoning
>   should apply, I think, and which is also used by a large number of
>   programs.)
I think making FS-based controls on by default is the least surprising
option for users.  It's what I suspect most programs intend, and it
would stop a lot of unexpected sandbox escapes.

Without FS-based controls, scoping is also necessary to prevent
sandbox escapes.  In my opinion, it's better to block access to
unprivileged services than to allow connecting to any service.

X11 is a trivial sandbox escape by taking over the user's GUI.
A sandboxed program accessing it indicates a misdesign.  The NSS module
is not needed by programs that do not do user database resolution,
which I suspect includes almost all sandboxed programs.

> I agree that the bug [1] has been asleep for a bit too long, and we
> should probably pick this up soon.  As we have not heard back from
> Ryan after our last inquiry, IMHO I think it would be fair to take it
> over.

I definitely support this!

> Apologies for the difficult feedback - I do not mean to get in the way
> of your enthusiasm here, but I would like to make sure that we don't
> implement this as a stop-gap measure just because the other bug [1]
> seemed more difficult and/or stuck in a github issue interaction.
> Let's rather unblock that bug, if that is the case. :)
> 
> As usual, I fully acknowledge that I might well be wrong and might
> have missed some of the underlying reasons, in which case I will
> happily be corrected and change my mind. :)
> 
> [1] https://github.com/landlock-lsm/linux/issues/36
> [2] https://github.com/landlock-lsm/linux/issues/36#issuecomment-3699749541
> [3] https://systemd.io/USER_GROUP_API/
> 
> Have a good start into the new year!
> –Günther
-- 
Sincerely,
Demi Marie Obenour (she/her/hers)

[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]

[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

end of thread, other threads:[~2025-12-31 16:54 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-12-30 17:20 [PATCH v2 0/6] Landlock: Implement scope control for pathname Unix sockets Tingmao Wang
2025-12-30 17:20 ` [PATCH v2 1/6] landlock: Add LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET scope bit to uAPI Tingmao Wang
2025-12-30 17:20 ` [PATCH v2 2/6] landlock: Implement LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET Tingmao Wang
2025-12-30 17:20 ` [PATCH v2 3/6] samples/landlock: Support LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET Tingmao Wang
2025-12-30 17:20 ` [PATCH v2 4/6] selftests/landlock: Support pathname socket path in set_unix_address Tingmao Wang
2025-12-30 17:20 ` [PATCH v2 5/6] selftests/landlock: Repurpose scoped_abstract_unix_test.c for pathname sockets too Tingmao Wang
2025-12-30 17:20 ` [PATCH v2 6/6] selftests/landlock: Add pathname socket variants for more tests Tingmao Wang
2025-12-30 23:16 ` [PATCH v2 0/6] Landlock: Implement scope control for pathname Unix sockets Günther Noack
2025-12-31 16:54   ` Demi Marie Obenour

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).