Skip to contentSkip to conversation

landlock: UNIX connect() control by pathname and scope

PATCH Superseded Günther Noack 8 participants 43 messages Feb 15 – Mar 17

Subsystems (7)

v5 of 8 Superseded Latest is v8 →

lore
Share & tools
lore
#0 lore raw

v5 cover landlock: UNIX connect() control by pathname and scope

Hello!

This patch set introduces a filesystem-based Landlock restriction mechanism for connecting to UNIX domain sockets (or addressing them with sendmsg(2)). It introduces the filesystem access right LANDLOCK_ACCESS_FS_RESOLVE_UNIX.

For the connection-oriented SOCK_STREAM and SOCK_SEQPACKET type sockets, the access right makes the connect(2) operation fail with EACCES, if denied.

SOCK_DGRAM-type UNIX sockets can be used both with connect(2), or by passing an explicit recipient address with every sendmsg(2) invocation. In the latter case, the Landlock check is done when an explicit recipient address is passed to sendmsg(2) and can make sendmsg(2) return EACCES. When UNIX datagram sockets are connected with connect(2), a fixed recipient address is associated with the socket and the check happens during connect(2) and may return EACCES.

When LANDLOCK_ACCESS_FS_RESOLVE_UNIX is handled within a Landlock domain, this domain will only allow connect(2) and sendmsg(2) to server sockets that were created within the same domain. Or, to phrase it the other way around: Unless it is allow-listed with a LANDLOCK_PATH_BENEATH rule, the newly created domain denies connect(2) and sendmsg(2) actions that are directed *outwards* of that domain. In that regard, LANDLOCK_ACCESS_FS_RESOLVE_UNIX has the same semantics as one of the "scoped" access rights.

== Motivation

Currently, landlocked processes can connect to named UNIX sockets through the BSD socket API described in unix(7), by invoking socket(2) followed by connect(2) with a suitable struct sockname_un holding the socket's filename. This is a surprising gap in Landlock's sandboxing capabilities for users (e.g. in [1]) and it can be used to escape a sandbox when a Unix service offers command execution (various such scenarios were listed by Tingmao Wang in [2]).

The original feature request is at [4].

== Alternatives and Related Work

=== Alternative: Use existing LSM hooks

We have carefully and seriously considered the use of existing LSM hooks, but still came to the conclusion that a new LSM hook is better suited in this case:

The existing hooks security_unix_stream_connect(), security_unix_may_send() and security_socket_connect() do not give access to the resolved filesystem path.

* Resolving the filesystem path in the struct sockaddr_un again within a Landlock would produce a TOCTOU race, so this is not an option.
* We would therefore need to wire through the resolved struct path from unix_find_bsd() to one of the existing LSM hooks which get called later. This would be a more substantial change to af_unix.c.

The struct path that is available in the listening-side struct sock is can be read through the existing hooks, but it is not an option to use this information: As the listening socket may have been bound from within a different namespace, the path that was used for that can is in the general case not meaningful for a sandboxed process. In particular, it is not possible to use this path (or prefixes thereof) when constructing a sandbox policy in the client-side process.

Paul Moore also chimed in in support of adding a new hook, with the rationale that the simplest change to the LSM hook interface has traditionally proven to be the most robust. [11]

More details are on the Github issue at [6] and on the LKML at [9].

In a the discussion of the V2 review, started by Christian Brauner [10], we have further explored the approach of reusing the existing LSM hooks but still ended up leaning on the side of introducing a new hook, with Paul Moore and me (gnoack) arguing for that option.

Further insights about the LSM hook were shared in the V3 review by Tingmao Wang [12], who spotted additional requirements due to the two approaches being merged into one patch set. The summary of that discussion is in [13].

=== Related work: Scope Control for Pathname Unix Sockets

The motivation for this patch is the same as in Tingmao Wang's patch set for "scoped" control for pathname Unix sockets [2], originally proposed in the Github feature request [5].

In [14], we have settled on the decision to merge the two patch sets into this one, whose primary way of controlling connect(2) is LANDLOCK_ACCESS_FS_RESOLVE_UNIX, but where this flag additionally has the semantics of only restricting this unix(7) IPC *outwards* of the created Landlock domain, in line with the logic that exists for the existing "scoped" flags already.

By having LANDLOCK_ACCESS_FS_RESOLVE_UNIX implement "scoping" semantics, we can avoid introducing two separate interacting flags for now, but we retain the option of introducing LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET at a later point in time, should such a flag be needed to express additional rules.

== Credits

The feature was originally suggested by Jann Horn in [7].

Tingmao Wang and Demi Marie Obenour have taken the initiative to revive this discussion again in [1], [4] and [5].

Tingmao Wang has sent the patch set for the scoped access control for pathname Unix sockets [2] and has contributed substantial insights during the code review, shaping the form of the LSM hook and agreeing to merge the pathname and scoped-flag patch sets.

Justin Suess has sent the patch for the LSM hook in [8] and subsequently through this patch set.

Christian Brauner and Paul Moore have contributed to the design of the new LSM hook, discussing the tradeoffs in [10].

Ryan Sullivan has started on an initial implementation and has brought up relevant discussion points on the Github issue at [4].

As maintainer of Landlock, Mickaël Salaün has done the main review so far and particularly pointed out ways in which the UNIX connect() patch sets interact with each other and what we need to look for with regards to UAPI consistency as Landlock evolves.

[1] https://lore.kernel.org/landlock/[email protected]/
[2] Tingmao Wang's "Implement scope control for pathname Unix sockets"

    https://lore.kernel.org/all/[email protected]/

[3] https://lore.kernel.org/all/[email protected]/
[4] Github issue for FS-based control for named Unix sockets:
https://github.com/landlock-lsm/linux/issues/36
[5] Github issue for scope-based restriction of named Unix sockets:
https://github.com/landlock-lsm/linux/issues/51
[6] https://github.com/landlock-lsm/linux/issues/36#issuecomment-2950632277
[7] https://lore.kernel.org/linux-security-module/CAG48ez3NvVnonOqKH4oRwRqbSOLO0p9djBqgvxVwn6gtGQBPcw@mail.gmail.com/
[8] Patch for the LSM hook:

    https://lore.kernel.org/all/[email protected]/

[9] https://lore.kernel.org/all/[email protected]/
[10] https://lore.kernel.org/all/20260113-kerngesund-etage-86de4a21da24@brauner/
[11] https://lore.kernel.org/all/CAHC9VhQHZCe0LMx4xzSo-h1SWY489U4frKYnxu4YVrcJN3x7nA@mail.gmail.com/
[12] https://lore.kernel.org/all/[email protected]/
[13] https://lore.kernel.org/all/[email protected]/
[14] https://lore.kernel.org/all/[email protected]/

---

== Patch set history

V1: https://lore.kernel.org/all/[email protected]/
V2: https://lore.kernel.org/all/[email protected]/
V3: https://lore.kernel.org/all/[email protected]/
V4: https://lore.kernel.org/all/[email protected]/

Changes in V5:

This change primarily adds tests, changing the testing approach for the main test to use the scoped_domains fixture as in Tingmao's patch set [2], and adding tests for the audit and coredump cases.

* Selftests:
* Replaced the main selftest with one based on scoped_domains
* Added audit test
* Added test for the coredump case
* Added a follow-up commit that simplifies ruleset enforcement
* Kernel code:
* Mark coredump check as unlikely (per Justin's review)
* Drop check for socket type (per Mickaël's review)

Changes in V4:

Since this version, this patch set subsumes the scoping semantics from Tingmao Wang's "Scope Control" patch set [2], per discussion with Tingmao Wang and Mickaël Salaün in [14] and in the thread leading up to it.

Now, LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET only restricts connect(2) and sendmsg(2) *outwards* of the domain where it is restricted, *with the same semantics as a "scoped" flag*.

* Implement a layer-mask based version of domain_is_scoped():
unmask_scoped_access(). Rationale: domain_is_scoped() returns early, which we can't do in the layer masks based variant. The two variants are similar enough.
* LSM hook: Replace 'type' argument with 'sk' argument,
per discussion in [12] and [13].
* Bump ABI version to 9 (pessimistically assuming that we won't make it for 7.0)
* Documentation fixes in header file and in Documentation/
* selftests: more test variants, now also parameterizing whether the server socket gets created within the Landlock domain or before that
* selftests: use EXPECT_EQ() for test cleanup

Changes in V3:
* LSM hook: rename it to security_unix_find() (Justin Suess) (resolving the previously open question about the LSM hook name) Related discussions:
https://lore.kernel.org/all/[email protected]/
https://lore.kernel.org/all/CAHC9VhSRiHwLEWfFkQdPEwgB4AXKbXzw_+3u=9hPpvUTnu02Bg@mail.gmail.com/
* Reunite the three UNIX resolving access rights back into one (resolving the previously open question about the access right structuring) Related discussion:
https://lore.kernel.org/all/[email protected]/)
* Sample tool: Add new UNIX lookup access rights to ACCESS_FILE

Changes in V2:
* Send Justin Suess's LSM hook patch together with the Landlock implementation
* LSM hook: Pass type and flags parameters to the hook, to make the access right more generally usable across LSMs, per suggestion from Paul Moore (Implemented by Justin)
* Split the access right into the three types of UNIX domain sockets:
SOCK_STREAM, SOCK_DGRAM and SOCK_SEQPACKET.
* selftests: More exhaustive tests.
* Removed a minor commit from V1 which adds a missing close(fd) to a test (it is already in the mic-next branch)

Günther Noack (8):
landlock: Control pathname UNIX domain socket resolution by path samples/landlock: Add support for named UNIX domain socket
restrictions
landlock/selftests: Test LANDLOCK_ACCESS_FS_RESOLVE_UNIX landlock/selftests: Audit test for LANDLOCK_ACCESS_FS_RESOLVE_UNIX landlock/selftests: Check that coredump sockets stay unrestricted landlock/selftests: fs_test: Simplify ruleset creation and enforcement
landlock: Document FS access right for pathname UNIX sockets
landlock: Document design rationale for scoped access rights

Justin Suess (1):
lsm: Add LSM hook security_unix_find

 Documentation/security/landlock.rst          |   38 +
 Documentation/userspace-api/landlock.rst     |   16 +-
 include/linux/lsm_hook_defs.h                |    5 +
 include/linux/security.h                     |   11 +
 include/uapi/linux/landlock.h                |   10 +
 net/unix/af_unix.c                           |    8 +
 samples/landlock/sandboxer.c                 |   15 +-
 security/landlock/access.h                   |   11 +-
 security/landlock/audit.c                    |    1 +
 security/landlock/fs.c                       |  102 +-
 security/landlock/limits.h                   |    2 +-
 security/landlock/syscalls.c                 |    2 +-
 security/security.c                          |   20 +
 tools/testing/selftests/landlock/base_test.c |    2 +-
 tools/testing/selftests/landlock/fs_test.c   | 1301 ++++++++++--------
 15 files changed, 942 insertions(+), 602 deletions(-)
Signature · 1 line
2.52.0
#1 lore raw

PATCH v5 1/9 lsm: Add LSM hook security_unix_find

From: Justin Suess <[email protected]>

Add a LSM hook security_unix_find.

This hook is called to check the path of a named unix socket before a connection is initiated. The peer socket may be inspected as well.

Why existing hooks are unsuitable:

Existing socket hooks, security_unix_stream_connect(), security_unix_may_send(), and security_socket_connect() don't provide TOCTOU-free / namespace independent access to the paths of sockets.

(1) We cannot resolve the path from the struct sockaddr in existing hooks. This requires another path lookup. A change in the path between the two lookups will cause a TOCTOU bug.

(2) We cannot use the struct path from the listening socket, because it may be bound to a path in a different namespace than the caller, resulting in a path that cannot be referenced at policy creation time.

Cc Günther Noack <[email protected]>
Cc Tingmao Wang <[email protected]>
---
 include/linux/lsm_hook_defs.h |  5 +++++
 include/linux/security.h      | 11 +++++++++++
 net/unix/af_unix.c            |  8 ++++++++
 security/security.c           | 20 ++++++++++++++++++++
 4 files changed, 44 insertions(+)
include/linux/lsm_hook_defs.h +5 -0
index 8c42b4bde09c..7a0fd3dbfa29 100644
@@ -317,6 +317,11 @@ LSM_HOOK(int, 0, post_notification, const struct cred *w_cred,
 LSM_HOOK(int, 0, watch_key, struct key *key)
 #endif /* CONFIG_SECURITY && CONFIG_KEY_NOTIFICATIONS */
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+LSM_HOOK(int, 0, unix_find, const struct path *path, struct sock *other,
+	 int flags)
+#endif /* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+
 #ifdef CONFIG_SECURITY_NETWORK
 LSM_HOOK(int, 0, unix_stream_connect, struct sock *sock, struct sock *other,
 	 struct sock *newsk)
include/linux/security.h +11 -0
index 83a646d72f6f..99a33d8eb28d 100644
@@ -1931,6 +1931,17 @@ static inline int security_mptcp_add_subflow(struct sock *sk, struct sock *ssk)
 }
 #endif	/* CONFIG_SECURITY_NETWORK */
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+
+int security_unix_find(const struct path *path, struct sock *other, int flags);
+
+#else /* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+static inline int security_unix_find(const struct path *path, struct sock *other, int flags)
+{
+	return 0;
+}
+#endif /* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+
 #ifdef CONFIG_SECURITY_INFINIBAND
 int security_ib_pkey_access(void *sec, u64 subnet_prefix, u16 pkey);
 int security_ib_endport_manage_subnet(void *sec, const char *name, u8 port_num);
net/unix/af_unix.c +8 -0
index d0511225799b..369812b79dd8 100644
@@ -1230,6 +1230,14 @@ static struct sock *unix_find_bsd(struct sockaddr_un *sunaddr, int addr_len,
 	if (!sk)
 		goto path_put;
 
+	/*
+	 * We call the hook because we know that the inode is a socket and we
+	 * hold a valid reference to it via the path.
+	 */
+	err = security_unix_find(&path, sk, flags);
+	if (err)
+		goto sock_put;
+
 	err = -EPROTOTYPE;
 	if (sk->sk_type == type)
 		touch_atime(&path);
security/security.c +20 -0
index 67af9228c4e9..c73196b8db4b 100644
@@ -4731,6 +4731,26 @@ int security_mptcp_add_subflow(struct sock *sk, struct sock *ssk)
 
 #endif	/* CONFIG_SECURITY_NETWORK */
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+/**
+ * security_unix_find() - Check if a named AF_UNIX socket can connect
+ * @path: path of the socket being connected to
+ * @other: peer sock
+ * @flags: flags associated with the socket
+ *
+ * This hook is called to check permissions before connecting to a named
+ * AF_UNIX socket.
+ *
+ * Return: Returns 0 if permission is granted.
+ */
+int security_unix_find(const struct path *path, struct sock *other, int flags)
+{
+	return call_int_hook(unix_find, path, other, flags);
+}
+EXPORT_SYMBOL(security_unix_find);
+
+#endif	/* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+
 #ifdef CONFIG_SECURITY_INFINIBAND
 /**
  * security_ib_pkey_access() - Check if access to an IB pkey is allowed
Signature · 1 line
2.52.0
maintainer
#2 lore raw

Re: [PATCH v5 1/9] lsm: Add LSM hook security_unix_find

Quoted 77 lines · Günther Noack

From: Justin Suess <[email protected]>

Add a LSM hook security_unix_find.

This hook is called to check the path of a named unix socket before a connection is initiated. The peer socket may be inspected as well.

Why existing hooks are unsuitable:

Existing socket hooks, security_unix_stream_connect(), security_unix_may_send(), and security_socket_connect() don't provide TOCTOU-free / namespace independent access to the paths of sockets.

(1) We cannot resolve the path from the struct sockaddr in existing hooks. This requires another path lookup. A change in the path between the two lookups will cause a TOCTOU bug.

(2) We cannot use the struct path from the listening socket, because it may be bound to a path in a different namespace than the caller, resulting in a path that cannot be referenced at policy creation time.

Cc Günther Noack <[email protected]>
Cc Tingmao Wang <[email protected]>
---
 include/linux/lsm_hook_defs.h |  5 +++++
 include/linux/security.h      | 11 +++++++++++
 net/unix/af_unix.c            |  8 ++++++++
 security/security.c           | 20 ++++++++++++++++++++
 4 files changed, 44 insertions(+)
include/linux/lsm_hook_defs.h +5 -0
index 8c42b4bde09c..7a0fd3dbfa29 100644
@@ -317,6 +317,11 @@ LSM_HOOK(int, 0, post_notification, const struct cred *w_cred,
 LSM_HOOK(int, 0, watch_key, struct key *key)
 #endif /* CONFIG_SECURITY && CONFIG_KEY_NOTIFICATIONS */
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+LSM_HOOK(int, 0, unix_find, const struct path *path, struct sock *other,
+	 int flags)
+#endif /* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+
 #ifdef CONFIG_SECURITY_NETWORK
 LSM_HOOK(int, 0, unix_stream_connect, struct sock *sock, struct sock *other,
 	 struct sock *newsk)
include/linux/security.h +11 -0
index 83a646d72f6f..99a33d8eb28d 100644
@@ -1931,6 +1931,17 @@ static inline int security_mptcp_add_subflow(struct sock *sk, struct sock *ssk)
 }
 #endif	/* CONFIG_SECURITY_NETWORK */
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+
+int security_unix_find(const struct path *path, struct sock *other, int flags);
+
+#else /* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+static inline int security_unix_find(const struct path *path, struct sock *other, int flags)
+{
+	return 0;
+}
+#endif /* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+
 #ifdef CONFIG_SECURITY_INFINIBAND
 int security_ib_pkey_access(void *sec, u64 subnet_prefix, u16 pkey);
 int security_ib_endport_manage_subnet(void *sec, const char *name, u8 port_num);
net/unix/af_unix.c +0 -0
index d0511225799b..369812b79dd8 100644
@@ -1230,6 +1230,14 @@ static struct sock *unix_find_bsd(struct sockaddr_un *sunaddr, int addr_len,
 	if (!sk)
 		goto path_put;
 

net/unix/af_unix.c static struct sock *unix_find_bsd(struct sockaddr_un *sunaddr, int addr_len, · line 1242

Quoted 10 lines
net/unix/af_unix.c +8 -0
+	/*
+	 * We call the hook because we know that the inode is a socket and we
+	 * hold a valid reference to it via the path.
+	 */
+	err = security_unix_find(&path, sk, flags);
+	if (err)
+		goto sock_put;
+
 	err = -EPROTOTYPE;
 	if (sk->sk_type == type)

I think this hook call should be moved here, just before the touch_atime() call for consistency with the socket type check, and to avoid doing useless check in the hook.

Quoted 36 lines
net/unix/af_unix.c +0 -0
 		touch_atime(&path);
security/security.c +20 -0
index 67af9228c4e9..c73196b8db4b 100644
@@ -4731,6 +4731,26 @@ int security_mptcp_add_subflow(struct sock *sk, struct sock *ssk)
 
 #endif	/* CONFIG_SECURITY_NETWORK */
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+/**
+ * security_unix_find() - Check if a named AF_UNIX socket can connect
+ * @path: path of the socket being connected to
+ * @other: peer sock
+ * @flags: flags associated with the socket
+ *
+ * This hook is called to check permissions before connecting to a named
+ * AF_UNIX socket.
+ *
+ * Return: Returns 0 if permission is granted.
+ */
+int security_unix_find(const struct path *path, struct sock *other, int flags)
+{
+	return call_int_hook(unix_find, path, other, flags);
+}
+EXPORT_SYMBOL(security_unix_find);
+
+#endif	/* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+
 #ifdef CONFIG_SECURITY_INFINIBAND
 /**
  * security_ib_pkey_access() - Check if access to an IB pkey is allowed
Signature · 1 line
2.52.0
#3 lore raw

Re: [PATCH v5 1/9] lsm: Add LSM hook security_unix_find

Quoted 78 lines · Mickaël Salaün
Quoted 77 lines · Günther Noack

From: Justin Suess <[email protected]>

Add a LSM hook security_unix_find.

This hook is called to check the path of a named unix socket before a connection is initiated. The peer socket may be inspected as well.

Why existing hooks are unsuitable:

Existing socket hooks, security_unix_stream_connect(), security_unix_may_send(), and security_socket_connect() don't provide TOCTOU-free / namespace independent access to the paths of sockets.

(1) We cannot resolve the path from the struct sockaddr in existing hooks. This requires another path lookup. A change in the path between the two lookups will cause a TOCTOU bug.

(2) We cannot use the struct path from the listening socket, because it may be bound to a path in a different namespace than the caller, resulting in a path that cannot be referenced at policy creation time.

Cc Günther Noack <[email protected]>
Cc Tingmao Wang <[email protected]>
---
 include/linux/lsm_hook_defs.h |  5 +++++
 include/linux/security.h      | 11 +++++++++++
 net/unix/af_unix.c            |  8 ++++++++
 security/security.c           | 20 ++++++++++++++++++++
 4 files changed, 44 insertions(+)
include/linux/lsm_hook_defs.h +5 -0
index 8c42b4bde09c..7a0fd3dbfa29 100644
@@ -317,6 +317,11 @@ LSM_HOOK(int, 0, post_notification, const struct cred *w_cred,
 LSM_HOOK(int, 0, watch_key, struct key *key)
 #endif /* CONFIG_SECURITY && CONFIG_KEY_NOTIFICATIONS */
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+LSM_HOOK(int, 0, unix_find, const struct path *path, struct sock *other,
+	 int flags)
+#endif /* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+
 #ifdef CONFIG_SECURITY_NETWORK
 LSM_HOOK(int, 0, unix_stream_connect, struct sock *sock, struct sock *other,
 	 struct sock *newsk)
include/linux/security.h +11 -0
index 83a646d72f6f..99a33d8eb28d 100644
@@ -1931,6 +1931,17 @@ static inline int security_mptcp_add_subflow(struct sock *sk, struct sock *ssk)
 }
 #endif	/* CONFIG_SECURITY_NETWORK */
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+
+int security_unix_find(const struct path *path, struct sock *other, int flags);
+
+#else /* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+static inline int security_unix_find(const struct path *path, struct sock *other, int flags)
+{
+	return 0;
+}
+#endif /* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+
 #ifdef CONFIG_SECURITY_INFINIBAND
 int security_ib_pkey_access(void *sec, u64 subnet_prefix, u16 pkey);
 int security_ib_endport_manage_subnet(void *sec, const char *name, u8 port_num);
net/unix/af_unix.c +0 -0
index d0511225799b..369812b79dd8 100644
@@ -1230,6 +1230,14 @@ static struct sock *unix_find_bsd(struct sockaddr_un *sunaddr, int addr_len,
 	if (!sk)
 		goto path_put;
 
Quoted 15 lines

net/unix/af_unix.c static struct sock *unix_find_bsd(struct sockaddr_un *sunaddr, int addr_len, · line 1242

Quoted 10 lines
net/unix/af_unix.c +8 -0
+	/*
+	 * We call the hook because we know that the inode is a socket and we
+	 * hold a valid reference to it via the path.
+	 */
+	err = security_unix_find(&path, sk, flags);
+	if (err)
+		goto sock_put;
+
 	err = -EPROTOTYPE;
 	if (sk->sk_type == type)

I think this hook call should be moved here, just before the touch_atime() call for consistency with the socket type check, and to avoid doing useless check in the hook.

Agreed. One less annoyance for end users of the hook is a win. I'll resend the hook with the call moved into this if (sk->sk_type == type) block for the next version.

Quoted 36 lines
Quoted 36 lines
net/unix/af_unix.c +0 -0
 		touch_atime(&path);
security/security.c +20 -0
index 67af9228c4e9..c73196b8db4b 100644
@@ -4731,6 +4731,26 @@ int security_mptcp_add_subflow(struct sock *sk, struct sock *ssk)
 
 #endif	/* CONFIG_SECURITY_NETWORK */
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+/**
+ * security_unix_find() - Check if a named AF_UNIX socket can connect
+ * @path: path of the socket being connected to
+ * @other: peer sock
+ * @flags: flags associated with the socket
+ *
+ * This hook is called to check permissions before connecting to a named
+ * AF_UNIX socket.
+ *
+ * Return: Returns 0 if permission is granted.
+ */
+int security_unix_find(const struct path *path, struct sock *other, int flags)
+{
+	return call_int_hook(unix_find, path, other, flags);
+}
+EXPORT_SYMBOL(security_unix_find);
+
+#endif	/* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+
 #ifdef CONFIG_SECURITY_INFINIBAND
 /**
  * security_ib_pkey_access() - Check if access to an IB pkey is allowed
Signature · 1 line
2.52.0
#4 lore raw

PATCH v6 1/1 lsm: Add LSM hook security_unix_find

Add a LSM hook security_unix_find.

This hook is called to check the path of a named unix socket before a connection is initiated. The peer socket may be inspected as well.

Why existing hooks are unsuitable:

Existing socket hooks, security_unix_stream_connect(), security_unix_may_send(), and security_socket_connect() don't provide TOCTOU-free / namespace independent access to the paths of sockets.

(1) We cannot resolve the path from the struct sockaddr in existing hooks. This requires another path lookup. A change in the path between the two lookups will cause a TOCTOU bug.

(2) We cannot use the struct path from the listening socket, because it may be bound to a path in a different namespace than the caller, resulting in a path that cannot be referenced at policy creation time.

Cc Günther Noack <[email protected]>
Cc Tingmao Wang <[email protected]>
---
 include/linux/lsm_hook_defs.h |  5 +++++
 include/linux/security.h      | 11 +++++++++++
 net/unix/af_unix.c            | 13 ++++++++++---
 security/security.c           | 20 ++++++++++++++++++++
 4 files changed, 46 insertions(+), 3 deletions(-)
include/linux/lsm_hook_defs.h +5 -0
index 8c42b4bde09c..7a0fd3dbfa29 100644
@@ -317,6 +317,11 @@ LSM_HOOK(int, 0, post_notification, const struct cred *w_cred,
 LSM_HOOK(int, 0, watch_key, struct key *key)
 #endif /* CONFIG_SECURITY && CONFIG_KEY_NOTIFICATIONS */
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+LSM_HOOK(int, 0, unix_find, const struct path *path, struct sock *other,
+	 int flags)
+#endif /* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+
 #ifdef CONFIG_SECURITY_NETWORK
 LSM_HOOK(int, 0, unix_stream_connect, struct sock *sock, struct sock *other,
 	 struct sock *newsk)
include/linux/security.h +11 -0
index 83a646d72f6f..99a33d8eb28d 100644
@@ -1931,6 +1931,17 @@ static inline int security_mptcp_add_subflow(struct sock *sk, struct sock *ssk)
 }
 #endif	/* CONFIG_SECURITY_NETWORK */
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+
+int security_unix_find(const struct path *path, struct sock *other, int flags);
+
+#else /* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+static inline int security_unix_find(const struct path *path, struct sock *other, int flags)
+{
+	return 0;
+}
+#endif /* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+
 #ifdef CONFIG_SECURITY_INFINIBAND
 int security_ib_pkey_access(void *sec, u64 subnet_prefix, u16 pkey);
 int security_ib_endport_manage_subnet(void *sec, const char *name, u8 port_num);
net/unix/af_unix.c +10 -3
index f6d56e70c7a2..41698460194b 100644
@@ -1231,10 +1231,17 @@ static struct sock *unix_find_bsd(struct sockaddr_un *sunaddr, int addr_len,
 		goto path_put;
 
 	err = -EPROTOTYPE;
-	if (sk->sk_type == type)
-		touch_atime(&path);
-	else
+	if (sk->sk_type != type)
+		goto sock_put;
+
+	/*
+	 * We call the hook because we know that the inode is a socket and we
+	 * hold a valid reference to it via the path.
+	 */
+	err = security_unix_find(&path, sk, flags);
+	if (err)
 		goto sock_put;
+	touch_atime(&path);
 
 	path_put(&path);
 
security/security.c +20 -0
index 67af9228c4e9..c73196b8db4b 100644
@@ -4731,6 +4731,26 @@ int security_mptcp_add_subflow(struct sock *sk, struct sock *ssk)
 
 #endif	/* CONFIG_SECURITY_NETWORK */
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+/**
+ * security_unix_find() - Check if a named AF_UNIX socket can connect
+ * @path: path of the socket being connected to
+ * @other: peer sock
+ * @flags: flags associated with the socket
+ *
+ * This hook is called to check permissions before connecting to a named
+ * AF_UNIX socket.
+ *
+ * Return: Returns 0 if permission is granted.
+ */
+int security_unix_find(const struct path *path, struct sock *other, int flags)
+{
+	return call_int_hook(unix_find, path, other, flags);
+}
+EXPORT_SYMBOL(security_unix_find);
+
+#endif	/* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+
 #ifdef CONFIG_SECURITY_INFINIBAND
 /**
  * security_ib_pkey_access() - Check if access to an IB pkey is allowed
Signature · 1 line
2.52.0
author Reviewed-by on v6 1/1
#5 lore raw

Re: [PATCH v6] lsm: Add LSM hook security_unix_find

Quoted 126 lines · Justin Suess

Add a LSM hook security_unix_find.

This hook is called to check the path of a named unix socket before a connection is initiated. The peer socket may be inspected as well.

Why existing hooks are unsuitable:

Existing socket hooks, security_unix_stream_connect(), security_unix_may_send(), and security_socket_connect() don't provide TOCTOU-free / namespace independent access to the paths of sockets.

(1) We cannot resolve the path from the struct sockaddr in existing hooks. This requires another path lookup. A change in the path between the two lookups will cause a TOCTOU bug.

(2) We cannot use the struct path from the listening socket, because it may be bound to a path in a different namespace than the caller, resulting in a path that cannot be referenced at policy creation time.

Cc Günther Noack <[email protected]>
Cc Tingmao Wang <[email protected]>
---
 include/linux/lsm_hook_defs.h |  5 +++++
 include/linux/security.h      | 11 +++++++++++
 net/unix/af_unix.c            | 13 ++++++++++---
 security/security.c           | 20 ++++++++++++++++++++
 4 files changed, 46 insertions(+), 3 deletions(-)
include/linux/lsm_hook_defs.h +5 -0
index 8c42b4bde09c..7a0fd3dbfa29 100644
@@ -317,6 +317,11 @@ LSM_HOOK(int, 0, post_notification, const struct cred *w_cred,
 LSM_HOOK(int, 0, watch_key, struct key *key)
 #endif /* CONFIG_SECURITY && CONFIG_KEY_NOTIFICATIONS */
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+LSM_HOOK(int, 0, unix_find, const struct path *path, struct sock *other,
+	 int flags)
+#endif /* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+
 #ifdef CONFIG_SECURITY_NETWORK
 LSM_HOOK(int, 0, unix_stream_connect, struct sock *sock, struct sock *other,
 	 struct sock *newsk)
include/linux/security.h +11 -0
index 83a646d72f6f..99a33d8eb28d 100644
@@ -1931,6 +1931,17 @@ static inline int security_mptcp_add_subflow(struct sock *sk, struct sock *ssk)
 }
 #endif	/* CONFIG_SECURITY_NETWORK */
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+
+int security_unix_find(const struct path *path, struct sock *other, int flags);
+
+#else /* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+static inline int security_unix_find(const struct path *path, struct sock *other, int flags)
+{
+	return 0;
+}
+#endif /* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+
 #ifdef CONFIG_SECURITY_INFINIBAND
 int security_ib_pkey_access(void *sec, u64 subnet_prefix, u16 pkey);
 int security_ib_endport_manage_subnet(void *sec, const char *name, u8 port_num);
net/unix/af_unix.c +10 -3
index f6d56e70c7a2..41698460194b 100644
@@ -1231,10 +1231,17 @@ static struct sock *unix_find_bsd(struct sockaddr_un *sunaddr, int addr_len,
 		goto path_put;
 
 	err = -EPROTOTYPE;
-	if (sk->sk_type == type)
-		touch_atime(&path);
-	else
+	if (sk->sk_type != type)
+		goto sock_put;
+
+	/*
+	 * We call the hook because we know that the inode is a socket and we
+	 * hold a valid reference to it via the path.
+	 */
+	err = security_unix_find(&path, sk, flags);
+	if (err)
 		goto sock_put;
+	touch_atime(&path);
 
 	path_put(&path);
 
security/security.c +20 -0
index 67af9228c4e9..c73196b8db4b 100644
@@ -4731,6 +4731,26 @@ int security_mptcp_add_subflow(struct sock *sk, struct sock *ssk)
 
 #endif	/* CONFIG_SECURITY_NETWORK */
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+/**
+ * security_unix_find() - Check if a named AF_UNIX socket can connect
+ * @path: path of the socket being connected to
+ * @other: peer sock
+ * @flags: flags associated with the socket
+ *
+ * This hook is called to check permissions before connecting to a named
+ * AF_UNIX socket.
+ *
+ * Return: Returns 0 if permission is granted.
+ */
+int security_unix_find(const struct path *path, struct sock *other, int flags)
+{
+	return call_int_hook(unix_find, path, other, flags);
+}
+EXPORT_SYMBOL(security_unix_find);
+
+#endif	/* CONFIG_SECURITY_NETWORK && CONFIG_SECURITY_PATH */
+
 #ifdef CONFIG_SECURITY_INFINIBAND
 /**
  * security_ib_pkey_access() - Check if access to an IB pkey is allowed
Signature · 1 line
2.52.0

Thank you, this looks good. I'll include it in the next version of the Unix connect patch set again.

–Günther

maintainer
#6 lore raw

Re: [PATCH v6] lsm: Add LSM hook security_unix_find

Quoted 29 lines · Günther Noack
Quoted 28 lines · Justin Suess

Add a LSM hook security_unix_find.

This hook is called to check the path of a named unix socket before a connection is initiated. The peer socket may be inspected as well.

Why existing hooks are unsuitable:

Existing socket hooks, security_unix_stream_connect(), security_unix_may_send(), and security_socket_connect() don't provide TOCTOU-free / namespace independent access to the paths of sockets.

(1) We cannot resolve the path from the struct sockaddr in existing hooks. This requires another path lookup. A change in the path between the two lookups will cause a TOCTOU bug.

(2) We cannot use the struct path from the listening socket, because it may be bound to a path in a different namespace than the caller, resulting in a path that cannot be referenced at policy creation time.

Cc Günther Noack <[email protected]>
Cc Tingmao Wang <[email protected]>
---
 include/linux/lsm_hook_defs.h |  5 +++++
 include/linux/security.h      | 11 +++++++++++
 net/unix/af_unix.c            | 13 ++++++++++---
 security/security.c           | 20 ++++++++++++++++++++
 4 files changed, 46 insertions(+), 3 deletions(-)

...

Quoted 4 lines

Thank you, this looks good. I'll include it in the next version of the Unix connect patch set again.

I'm looking for this patchset to review/ACK the new hook in context, but I'm not seeing it in my inbox or lore. Did I simply miss the patchset or is it still a work in progress? No worries if it hasn't been posted yet, I just wanted to make sure I wasn't holding this up any more than I already may have :)

Signature · 1 line
paul-moore.com
#7 lore raw

Re: [PATCH v6] lsm: Add LSM hook security_unix_find

Quoted 35 lines · Paul Moore
Quoted 29 lines · Günther Noack
Quoted 28 lines · Justin Suess

Add a LSM hook security_unix_find.

This hook is called to check the path of a named unix socket before a connection is initiated. The peer socket may be inspected as well.

Why existing hooks are unsuitable:

Existing socket hooks, security_unix_stream_connect(), security_unix_may_send(), and security_socket_connect() don't provide TOCTOU-free / namespace independent access to the paths of sockets.

(1) We cannot resolve the path from the struct sockaddr in existing hooks. This requires another path lookup. A change in the path between the two lookups will cause a TOCTOU bug.

(2) We cannot use the struct path from the listening socket, because it may be bound to a path in a different namespace than the caller, resulting in a path that cannot be referenced at policy creation time.

Cc Günther Noack <[email protected]>
Cc Tingmao Wang <[email protected]>
---
 include/linux/lsm_hook_defs.h |  5 +++++
 include/linux/security.h      | 11 +++++++++++
 net/unix/af_unix.c            | 13 ++++++++++---
 security/security.c           | 20 ++++++++++++++++++++
 4 files changed, 46 insertions(+), 3 deletions(-)

...

Quoted 2 lines
Quoted 9 lines
Quoted 2 lines

Thank you, this looks good. I'll include it in the next version of the Unix connect patch set again.

I'm looking for this patchset to review/ACK the new hook in context, but I'm not seeing it in my inbox or lore. Did I simply miss the patchset or is it still a work in progress? No worries if it hasn't been posted yet, I just wanted to make sure I wasn't holding this up any more than I already may have :)

Good Morning Paul,

Can't speak to the rest of the patch, but I sent this LSM hook for review purposes before inclusion with the rest of the V6 of this patch.

Günther added his review tag, but I was asked to make some minor comment / commit message updates. I sent the same patch, with updated comments/commit to him in a follow up, off-list email to avoid spamming the list. No code changes were made, just comments.

I don't think this particular patch will change substantially, unless we find something unexpected. But the way we use the hook may change (esp wrt to locking and the SOCK_DEAD state), which is important for your review.

So you may want to hold off your review until the full V6 series gets sent so you can review the hook in context. There were some questions about locking that needed proper digging into. [1]

Thank you for your time.

Justin

[1]: https://lore.kernel.org/linux-security-module/[email protected]/

Quoted 2 lines
Signature · 1 line
paul-moore.com
maintainer
#8 lore raw

Re: [PATCH v6] lsm: Add LSM hook security_unix_find

Quoted 56 lines · Justin Suess
Quoted 35 lines · Paul Moore
Quoted 29 lines · Günther Noack
Quoted 28 lines · Justin Suess

Add a LSM hook security_unix_find.

This hook is called to check the path of a named unix socket before a connection is initiated. The peer socket may be inspected as well.

Why existing hooks are unsuitable:

Existing socket hooks, security_unix_stream_connect(), security_unix_may_send(), and security_socket_connect() don't provide TOCTOU-free / namespace independent access to the paths of sockets.

(1) We cannot resolve the path from the struct sockaddr in existing hooks. This requires another path lookup. A change in the path between the two lookups will cause a TOCTOU bug.

(2) We cannot use the struct path from the listening socket, because it may be bound to a path in a different namespace than the caller, resulting in a path that cannot be referenced at policy creation time.

Cc Günther Noack <[email protected]>
Cc Tingmao Wang <[email protected]>
---
 include/linux/lsm_hook_defs.h |  5 +++++
 include/linux/security.h      | 11 +++++++++++
 net/unix/af_unix.c            | 13 ++++++++++---
 security/security.c           | 20 ++++++++++++++++++++
 4 files changed, 46 insertions(+), 3 deletions(-)

...

Quoted 2 lines
Quoted 8 lines
Quoted 2 lines

Thank you, this looks good. I'll include it in the next version of the Unix connect patch set again.

I'm looking for this patchset to review/ACK the new hook in context, but I'm not seeing it in my inbox or lore. Did I simply miss the patchset or is it still a work in progress? No worries if it hasn't been posted yet, I just wanted to make sure I wasn't holding this up any more than I already may have :)

Good Morning Paul,

Can't speak to the rest of the patch, but I sent this LSM hook for review purposes before inclusion with the rest of the V6 of this patch.

Günther added his review tag, but I was asked to make some minor comment / commit message updates. I sent the same patch, with updated comments/commit to him in a follow up, off-list email to avoid spamming the list. No code changes were made, just comments.

Quoted 7 lines

I don't think this particular patch will change substantially, unless we find something unexpected. But the way we use the hook may change (esp wrt to locking and the SOCK_DEAD state), which is important for your review.

So you may want to hold off your review until the full V6 series gets sent so you can review the hook in context. There were some questions about locking that needed proper digging into. [1]

Great, thanks for the update, that was helpful. As you recommend, I'll hold off on reviewing this further until we have the full context of the other patchset; we've already talked about this hook addition a few times anyway, and based on a quick look yesterday, nothing particularly evil jumped out at me.

Signature · 1 line
paul-moore.com
#9 lore raw

Re: [PATCH v6] lsm: Add LSM hook security_unix_find

Quoted 65 lines · Paul Moore
Quoted 56 lines · Justin Suess
Quoted 35 lines · Paul Moore
Quoted 29 lines · Günther Noack
Quoted 28 lines · Justin Suess

Add a LSM hook security_unix_find.

This hook is called to check the path of a named unix socket before a connection is initiated. The peer socket may be inspected as well.

Why existing hooks are unsuitable:

Existing socket hooks, security_unix_stream_connect(), security_unix_may_send(), and security_socket_connect() don't provide TOCTOU-free / namespace independent access to the paths of sockets.

(1) We cannot resolve the path from the struct sockaddr in existing hooks. This requires another path lookup. A change in the path between the two lookups will cause a TOCTOU bug.

(2) We cannot use the struct path from the listening socket, because it may be bound to a path in a different namespace than the caller, resulting in a path that cannot be referenced at policy creation time.

Cc Günther Noack <[email protected]>
Cc Tingmao Wang <[email protected]>
---
 include/linux/lsm_hook_defs.h |  5 +++++
 include/linux/security.h      | 11 +++++++++++
 net/unix/af_unix.c            | 13 ++++++++++---
 security/security.c           | 20 ++++++++++++++++++++
 4 files changed, 46 insertions(+), 3 deletions(-)

...

Quoted 2 lines
Quoted 8 lines
Quoted 2 lines

Thank you, this looks good. I'll include it in the next version of the Unix connect patch set again.

I'm looking for this patchset to review/ACK the new hook in context, but I'm not seeing it in my inbox or lore. Did I simply miss the patchset or is it still a work in progress? No worries if it hasn't been posted yet, I just wanted to make sure I wasn't holding this up any more than I already may have :)

Good Morning Paul,

Can't speak to the rest of the patch, but I sent this LSM hook for review purposes before inclusion with the rest of the V6 of this patch.

Günther added his review tag, but I was asked to make some minor comment / commit message updates. I sent the same patch, with updated comments/commit to him in a follow up, off-list email to avoid spamming the list. No code changes were made, just comments.

Quoted 7 lines

I don't think this particular patch will change substantially, unless we find something unexpected. But the way we use the hook may change (esp wrt to locking and the SOCK_DEAD state), which is important for your review.

So you may want to hold off your review until the full V6 series gets sent so you can review the hook in context. There were some questions about locking that needed proper digging into. [1]

Quoted 5 lines

Great, thanks for the update, that was helpful. As you recommend, I'll hold off on reviewing this further until we have the full context of the other patchset; we've already talked about this hook addition a few times anyway, and based on a quick look yesterday, nothing particularly evil jumped out at me.

Yes, thanks - I have been busy with the TSYNC fixes recently, which were more urgent because it's in the RC for 7.0, but will get back to the UNIX restrictions soon.

—Günther

#10 lore raw

Re: [PATCH v6] lsm: Add LSM hook security_unix_find

In reply to Justin Suess

Hello!

Quoted 7 lines · Justin Suess
security/security.c +0 -0
index 67af9228c4e9..c73196b8db4b 100644
@@ -4731,6 +4731,26 @@ int security_mptcp_add_subflow(struct sock *sk, struct sock *ssk)
 
 #endif	/* CONFIG_SECURITY_NETWORK */

security/security.c int security_mptcp_add_subflow(struct sock *sk, struct sock *ssk) · line 4742

Quoted 10 lines
security/security.c +9 -0
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+/**
+ * security_unix_find() - Check if a named AF_UNIX socket can connect
+ * @path: path of the socket being connected to
+ * @other: peer sock
+ * @flags: flags associated with the socket
+ *
+ * This hook is called to check permissions before connecting to a named
+ * AF_UNIX socket.

Nit: Could we please insert a sentence about locking here?

Something like:

The caller holds no locks on @other.

(Originally brought up by Mickaël in
https://lore.kernel.org/all/[email protected]/)

Thanks,
–Günther

#11 lore raw

Re: [PATCH v6] lsm: Add LSM hook security_unix_find

Quoted 25 lines · Günther Noack

Hello!

Quoted 7 lines · Justin Suess
security/security.c +0 -0
index 67af9228c4e9..c73196b8db4b 100644
@@ -4731,6 +4731,26 @@ int security_mptcp_add_subflow(struct sock *sk, struct sock *ssk)
 
 #endif	/* CONFIG_SECURITY_NETWORK */

security/security.c int security_mptcp_add_subflow(struct sock *sk, struct sock *ssk) · line 4742

Quoted 10 lines
security/security.c +9 -0
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+/**
+ * security_unix_find() - Check if a named AF_UNIX socket can connect
+ * @path: path of the socket being connected to
+ * @other: peer sock
+ * @flags: flags associated with the socket
+ *
+ * This hook is called to check permissions before connecting to a named
+ * AF_UNIX socket.

Nit: Could we please insert a sentence about locking here?

Something like:

Quoted 7 lines

The caller holds no locks on @other.

(Originally brought up by Mickaël in
https://lore.kernel.org/all/[email protected]/)

Thanks,
–Günther

Sounds good. Would a "Link:" to the mentioned thread be appropriate in the commit message?

I feel like the reasoning for this is subtle but important for hook consumers.

Justin

maintainer
#12 lore raw

Re: [PATCH v6] lsm: Add LSM hook security_unix_find

Quoted 28 lines · Justin Suess
Quoted 27 lines · Günther Noack

Hello!

Quoted 7 lines · Justin Suess
security/security.c +0 -0
index 67af9228c4e9..c73196b8db4b 100644
@@ -4731,6 +4731,26 @@ int security_mptcp_add_subflow(struct sock *sk, struct sock *ssk)
 
 #endif	/* CONFIG_SECURITY_NETWORK */

security/security.c int security_mptcp_add_subflow(struct sock *sk, struct sock *ssk) · line 4742

Quoted 10 lines
security/security.c +9 -0
 
+#if defined(CONFIG_SECURITY_NETWORK) && defined(CONFIG_SECURITY_PATH)
+/**
+ * security_unix_find() - Check if a named AF_UNIX socket can connect
+ * @path: path of the socket being connected to
+ * @other: peer sock
+ * @flags: flags associated with the socket
+ *
+ * This hook is called to check permissions before connecting to a named
+ * AF_UNIX socket.

Nit: Could we please insert a sentence about locking here?

Something like:

The caller holds no locks on @other.

Quoted 7 lines
Quoted 5 lines

(Originally brought up by Mickaël in
https://lore.kernel.org/all/[email protected]/)

Thanks,
–Günther

Sounds good. Would a "Link:" to the mentioned thread be appropriate in the commit message?

Feel free to include relevant parts of our discussion in the commit message, which would make a Link redundant. I think a Link is useful if the commit message doesn't contain the whole context or misses information, which is often the case wrt discussions or long emails.

Quoted 3 lines

I feel like the reasoning for this is subtle but important for hook consumers.

Indeed. That should be explained in the hook comment.

Quoted 3 lines

Justin

#13 lore raw

PATCH v5 2/9 landlock: Control pathname UNIX domain socket resolution by path

In reply to Günther Noack

* Add a new access right LANDLOCK_ACCESS_FS_RESOLVE_UNIX, which controls the look up operations for named UNIX domain sockets. The resolution happens during connect() and sendmsg() (depending on socket type).
* Hook into the path lookup in unix_find_bsd() in af_unix.c, using a LSM hook. Make policy decisions based on the new access rights
* Increment the Landlock ABI version.
* Minor test adaptions to keep the tests working.

With this access right, access is granted if either of the following conditions is met:

* The target socket's filesystem path was allow-listed using a LANDLOCK_RULE_PATH_BENEATH rule, *or*:
* The target socket was created in the same Landlock domain in which LANDLOCK_ACCESS_FS_RESOLVE_UNIX was restricted.

In case of a denial, connect() and sendmsg() return EACCES, which is the same error as it is returned if the user does not have the write bit in the traditional Unix file system permissions of that file.

This feature was created with substantial discussion and input from Justin Suess, Tingmao Wang and Mickaël Salaün.

---
 include/uapi/linux/landlock.h                |  10 ++
 security/landlock/access.h                   |  11 +-
 security/landlock/audit.c                    |   1 +
 security/landlock/fs.c                       | 102 ++++++++++++++++++-
 security/landlock/limits.h                   |   2 +-
 security/landlock/syscalls.c                 |   2 +-
 tools/testing/selftests/landlock/base_test.c |   2 +-
 tools/testing/selftests/landlock/fs_test.c   |   5 +-
 8 files changed, 128 insertions(+), 7 deletions(-)
include/uapi/linux/landlock.h +10 -0
index f88fa1f68b77..3a8fc3af0d64 100644
@@ -248,6 +248,15 @@ struct landlock_net_port_attr {
  *
  *   This access right is available since the fifth version of the Landlock
  *   ABI.
+ * - %LANDLOCK_ACCESS_FS_RESOLVE_UNIX: Look up pathname UNIX domain sockets
+ *   (:manpage:`unix(7)`).  On UNIX domain sockets, this restricts both calls to
+ *   :manpage:`connect(2)` as well as calls to :manpage:`sendmsg(2)` with an
+ *   explicit recipient address.
+ *
+ *   This access right only applies to connections to UNIX server sockets which
+ *   were created outside of the newly created Landlock domain (e.g. from within
+ *   a parent domain or from an unrestricted process).  Newly created UNIX
+ *   servers within the same Landlock domain continue to be accessible.
  *
  * Whether an opened file can be truncated with :manpage:`ftruncate(2)` or used
  * with `ioctl(2)` is determined during :manpage:`open(2)`, in the same way as
@@ -333,6 +342,7 @@ struct landlock_net_port_attr {
 #define LANDLOCK_ACCESS_FS_REFER			(1ULL << 13)
 #define LANDLOCK_ACCESS_FS_TRUNCATE			(1ULL << 14)
 #define LANDLOCK_ACCESS_FS_IOCTL_DEV			(1ULL << 15)
+#define LANDLOCK_ACCESS_FS_RESOLVE_UNIX			(1ULL << 16)
 /* clang-format on */
 
 /**
security/landlock/access.h +10 -1
index 42c95747d7bd..9a2991688835 100644
@@ -34,7 +34,7 @@
 	LANDLOCK_ACCESS_FS_IOCTL_DEV)
 /* clang-format on */
 
-typedef u16 access_mask_t;
+typedef u32 access_mask_t;
 
 /* Makes sure all filesystem access rights can be stored. */
 static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_ACCESS_FS);
@@ -76,6 +76,15 @@ struct layer_access_masks {
 	access_mask_t access[LANDLOCK_MAX_NUM_LAYERS];
 };
 
+static inline bool
+layer_access_masks_empty(const struct layer_access_masks *masks)
+{
+	for (size_t i = 0; i < ARRAY_SIZE(masks->access); i++)
+		if (masks->access[i])
+			return false;
+	return true;
+}
+
 /*
  * Tracks domains responsible of a denied access.  This avoids storing in each
  * object the full matrix of per-layer unfulfilled access rights, which is
security/landlock/audit.c +1 -0
index 60ff217ab95b..8d0edf94037d 100644
@@ -37,6 +37,7 @@ static const char *const fs_access_strings[] = {
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev",
+	[BIT_INDEX(LANDLOCK_ACCESS_FS_RESOLVE_UNIX)] = "fs.resolve_unix",
 };
 
 static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);
security/landlock/fs.c +101 -1

126 lines not on this page · raw message

security/landlock/limits.h +1 -1
index eb584f47288d..b454ad73b15e 100644
@@ -19,7 +19,7 @@
 #define LANDLOCK_MAX_NUM_LAYERS		16
 #define LANDLOCK_MAX_NUM_RULES		U32_MAX
 
-#define LANDLOCK_LAST_ACCESS_FS		LANDLOCK_ACCESS_FS_IOCTL_DEV
+#define LANDLOCK_LAST_ACCESS_FS		LANDLOCK_ACCESS_FS_RESOLVE_UNIX
 #define LANDLOCK_MASK_ACCESS_FS		((LANDLOCK_LAST_ACCESS_FS << 1) - 1)
 #define LANDLOCK_NUM_ACCESS_FS		__const_hweight64(LANDLOCK_MASK_ACCESS_FS)
 
security/landlock/syscalls.c +1 -1
index 0d66a68677b7..933902d43241 100644
@@ -164,7 +164,7 @@ static const struct file_operations ruleset_fops = {
  * If the change involves a fix that requires userspace awareness, also update
  * the errata documentation in Documentation/userspace-api/landlock.rst .
  */
-const int landlock_abi_version = 8;
+const int landlock_abi_version = 9;
 
 /**
  * sys_landlock_create_ruleset - Create a new ruleset
tools/testing/selftests/landlock/base_test.c +1 -1
index 0fea236ef4bd..30d37234086c 100644
@@ -76,7 +76,7 @@ TEST(abi_version)
 	const struct landlock_ruleset_attr ruleset_attr = {
 		.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
 	};
-	ASSERT_EQ(8, landlock_create_ruleset(NULL, 0,
+	ASSERT_EQ(9, landlock_create_ruleset(NULL, 0,
 					     LANDLOCK_CREATE_RULESET_VERSION));
 
 	ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
tools/testing/selftests/landlock/fs_test.c +3 -2
index 968a91c927a4..b318627e7561 100644
@@ -575,9 +575,10 @@ TEST_F_FORK(layout1, inval)
 	LANDLOCK_ACCESS_FS_WRITE_FILE | \
 	LANDLOCK_ACCESS_FS_READ_FILE | \
 	LANDLOCK_ACCESS_FS_TRUNCATE | \
-	LANDLOCK_ACCESS_FS_IOCTL_DEV)
+	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
 
-#define ACCESS_LAST LANDLOCK_ACCESS_FS_IOCTL_DEV
+#define ACCESS_LAST LANDLOCK_ACCESS_FS_RESOLVE_UNIX
 
 #define ACCESS_ALL ( \
 	ACCESS_FILE | \
Signature · 1 line
2.52.0
maintainer
#14 lore raw

Re: [PATCH v5 2/9] landlock: Control pathname UNIX domain socket resolution by path

Quoted 48 lines · Günther Noack

* Add a new access right LANDLOCK_ACCESS_FS_RESOLVE_UNIX, which controls the look up operations for named UNIX domain sockets. The resolution happens during connect() and sendmsg() (depending on socket type).
* Hook into the path lookup in unix_find_bsd() in af_unix.c, using a LSM hook. Make policy decisions based on the new access rights
* Increment the Landlock ABI version.
* Minor test adaptions to keep the tests working.

With this access right, access is granted if either of the following conditions is met:

* The target socket's filesystem path was allow-listed using a LANDLOCK_RULE_PATH_BENEATH rule, *or*:
* The target socket was created in the same Landlock domain in which LANDLOCK_ACCESS_FS_RESOLVE_UNIX was restricted.

In case of a denial, connect() and sendmsg() return EACCES, which is the same error as it is returned if the user does not have the write bit in the traditional Unix file system permissions of that file.

This feature was created with substantial discussion and input from Justin Suess, Tingmao Wang and Mickaël Salaün.

---
 include/uapi/linux/landlock.h                |  10 ++
 security/landlock/access.h                   |  11 +-
 security/landlock/audit.c                    |   1 +
 security/landlock/fs.c                       | 102 ++++++++++++++++++-
 security/landlock/limits.h                   |   2 +-
 security/landlock/syscalls.c                 |   2 +-
 tools/testing/selftests/landlock/base_test.c |   2 +-
 tools/testing/selftests/landlock/fs_test.c   |   5 +-
 8 files changed, 128 insertions(+), 7 deletions(-)
include/uapi/linux/landlock.h +0 -0
index f88fa1f68b77..3a8fc3af0d64 100644
@@ -248,6 +248,15 @@ struct landlock_net_port_attr {
  *
  *   This access right is available since the fifth version of the Landlock

include/uapi/linux/landlock.h struct landlock_net_port_attr { · line 259

Quoted 10 lines
include/uapi/linux/landlock.h +9 -0
  *   ABI.
+ * - %LANDLOCK_ACCESS_FS_RESOLVE_UNIX: Look up pathname UNIX domain sockets
+ *   (:manpage:`unix(7)`).  On UNIX domain sockets, this restricts both calls to
+ *   :manpage:`connect(2)` as well as calls to :manpage:`sendmsg(2)` with an
+ *   explicit recipient address.
+ *
+ *   This access right only applies to connections to UNIX server sockets which
+ *   were created outside of the newly created Landlock domain (e.g. from within
+ *   a parent domain or from an unrestricted process).  Newly created UNIX
+ *   servers within the same Landlock domain continue to be accessible.

It might help to add a reference to the explicit scope mechanism.

Please squash patch 9/9 into this one and also add a reference here to the rationale described in security/landlock.rst

Quoted 84 lines
include/uapi/linux/landlock.h +1 -0
  *
  * Whether an opened file can be truncated with :manpage:`ftruncate(2)` or used
  * with `ioctl(2)` is determined during :manpage:`open(2)`, in the same way as
@@ -333,6 +342,7 @@ struct landlock_net_port_attr {
 #define LANDLOCK_ACCESS_FS_REFER			(1ULL << 13)
 #define LANDLOCK_ACCESS_FS_TRUNCATE			(1ULL << 14)
 #define LANDLOCK_ACCESS_FS_IOCTL_DEV			(1ULL << 15)
+#define LANDLOCK_ACCESS_FS_RESOLVE_UNIX			(1ULL << 16)
 /* clang-format on */
 
 /**
security/landlock/access.h +10 -1
index 42c95747d7bd..9a2991688835 100644
@@ -34,7 +34,7 @@
 	LANDLOCK_ACCESS_FS_IOCTL_DEV)
 /* clang-format on */
 
-typedef u16 access_mask_t;
+typedef u32 access_mask_t;
 
 /* Makes sure all filesystem access rights can be stored. */
 static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_ACCESS_FS);
@@ -76,6 +76,15 @@ struct layer_access_masks {
 	access_mask_t access[LANDLOCK_MAX_NUM_LAYERS];
 };
 
+static inline bool
+layer_access_masks_empty(const struct layer_access_masks *masks)
+{
+	for (size_t i = 0; i < ARRAY_SIZE(masks->access); i++)
+		if (masks->access[i])
+			return false;
+	return true;
+}
+
 /*
  * Tracks domains responsible of a denied access.  This avoids storing in each
  * object the full matrix of per-layer unfulfilled access rights, which is
security/landlock/audit.c +1 -0
index 60ff217ab95b..8d0edf94037d 100644
@@ -37,6 +37,7 @@ static const char *const fs_access_strings[] = {
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev",
+	[BIT_INDEX(LANDLOCK_ACCESS_FS_RESOLVE_UNIX)] = "fs.resolve_unix",
 };
 
 static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);
security/landlock/fs.c +9 -1
index e764470f588c..76035c6f2bf1 100644
@@ -27,6 +27,7 @@
 #include <linux/lsm_hooks.h>
 #include <linux/mount.h>
 #include <linux/namei.h>
+#include <linux/net.h>
 #include <linux/path.h>
 #include <linux/pid.h>
 #include <linux/rcupdate.h>
@@ -314,7 +315,8 @@ static struct landlock_object *get_inode_object(struct inode *const inode)
 	LANDLOCK_ACCESS_FS_WRITE_FILE | \
 	LANDLOCK_ACCESS_FS_READ_FILE | \
 	LANDLOCK_ACCESS_FS_TRUNCATE | \
-	LANDLOCK_ACCESS_FS_IOCTL_DEV)
+	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
 /* clang-format on */
 
 /*
@@ -1561,6 +1563,103 @@ static int hook_path_truncate(const struct path *const path)
 	return current_check_access_path(path, LANDLOCK_ACCESS_FS_TRUNCATE);
 }
 
+/**
+ * unmask_scoped_access - Remove access right bits in @masks in all layers
+ *                        where @client and @server have the same domain
+ *
+ * This does the same as domain_is_scoped(), but unmasks bits in @masks.
+ * It can not return early as domain_is_scoped() does.

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1581

Quoted 10 lines
security/landlock/fs.c +10 -0
+ *
+ * @client: Client domain
+ * @server: Server domain
+ * @masks: Layer access masks to unmask
+ * @access: Access bit that controls scoping
+ */
+static void unmask_scoped_access(const struct landlock_ruleset *const client,
+				 const struct landlock_ruleset *const server,
+				 struct layer_access_masks *const masks,
+				 const access_mask_t access)

This helper should be moved to task.c and factored out with domain_is_scoped(). This should be a dedicated patch.

Quoted 55 lines
security/landlock/fs.c +55 -0
+{
+	int client_layer, server_layer;
+	const struct landlock_hierarchy *client_walker, *server_walker;
+
+	if (WARN_ON_ONCE(!client))
+		return; /* should not happen */
+
+	if (!server)
+		return; /* server has no Landlock domain; nothing to clear */
+
+	client_layer = client->num_layers - 1;
+	client_walker = client->hierarchy;
+	server_layer = server->num_layers - 1;
+	server_walker = server->hierarchy;
+
+	/*
+	 * Clears the access bits at all layers where the client domain is the
+	 * same as the server domain.  We start the walk at min(client_layer,
+	 * server_layer).  The layer bits until there can not be cleared because
+	 * either the client or the server domain is missing.
+	 */
+	for (; client_layer > server_layer; client_layer--)
+		client_walker = client_walker->parent;
+
+	for (; server_layer > client_layer; server_layer--)
+		server_walker = server_walker->parent;
+
+	for (; client_layer >= 0; client_layer--) {
+		if (masks->access[client_layer] & access &&
+		    client_walker == server_walker)
+			masks->access[client_layer] &= ~access;
+
+		client_walker = client_walker->parent;
+		server_walker = server_walker->parent;
+	}
+}
+
+static int hook_unix_find(const struct path *const path, struct sock *other,
+			  int flags)
+{
+	const struct landlock_ruleset *dom_other;
+	const struct landlock_cred_security *subject;
+	struct layer_access_masks layer_masks;
+	struct landlock_request request = {};
+	static const struct access_masks fs_resolve_unix = {
+		.fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+	};
+
+	/* Lookup for the purpose of saving coredumps is OK. */
+	if (unlikely(flags & SOCK_COREDUMP))
+		return 0;
+
+	/* Access to the same (or a lower) domain is always allowed. */
+	subject = landlock_get_applicable_subject(current_cred(),
+						  fs_resolve_unix, NULL);

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1646

Quoted 10 lines
security/landlock/fs.c +10 -0
+
+	if (!subject)
+		return 0;
+
+	if (!landlock_init_layer_masks(subject->domain, fs_resolve_unix.fs,
+				       &layer_masks, LANDLOCK_KEY_INODE))
+		return 0;
+
+	/* Checks the layers in which we are connecting within the same domain. */
+	dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain;

We need to call unix_state_lock(other) before reading it, and check for SOCK_DEAD, and check sk_socket before dereferencing it. Indeed, the socket can be make orphan (see unix_dgram_sendmsg and unix_stream_connect). I *think* a socket cannot be "resurrected" or recycled once dead, so we may assume there is no race condition wrt dom_other, but please double check. This lockless call should be made clear in the LSM hook. It's OK to not lock the socket before security_unix_find() (1) because no LSM might implement and (2) they might not need to lock the socket (e.g. if the caller is not sandboxed).

The updated code should look something like this:

unix_state_unlock(other);

if (unlikely(sock_flag(other, SOCK_DEAD) || !other->sk_socket)) {
	unix_state_unlock(other);
	return 0;
}

dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain; unix_state_unlock(other);

Quoted 87 lines
security/landlock/fs.c +17 -0
+	unmask_scoped_access(subject->domain, dom_other, &layer_masks,
+			     fs_resolve_unix.fs);
+
+	if (layer_access_masks_empty(&layer_masks))
+		return 0;
+
+	/* Checks the connections to allow-listed paths. */
+	if (is_access_to_paths_allowed(subject->domain, path,
+				       fs_resolve_unix.fs, &layer_masks,
+				       &request, NULL, 0, NULL, NULL, NULL))
+		return 0;
+
+	landlock_log_denial(subject, &request);
+	return -EACCES;
+}
+
 /* File hooks */
 
 /**
@@ -1838,6 +1937,7 @@ static struct security_hook_list landlock_hooks[] __ro_after_init = {
 	LSM_HOOK_INIT(path_unlink, hook_path_unlink),
 	LSM_HOOK_INIT(path_rmdir, hook_path_rmdir),
 	LSM_HOOK_INIT(path_truncate, hook_path_truncate),
+	LSM_HOOK_INIT(unix_find, hook_unix_find),
 
 	LSM_HOOK_INIT(file_alloc_security, hook_file_alloc_security),
 	LSM_HOOK_INIT(file_open, hook_file_open),
security/landlock/limits.h +1 -1
index eb584f47288d..b454ad73b15e 100644
@@ -19,7 +19,7 @@
 #define LANDLOCK_MAX_NUM_LAYERS		16
 #define LANDLOCK_MAX_NUM_RULES		U32_MAX
 
-#define LANDLOCK_LAST_ACCESS_FS		LANDLOCK_ACCESS_FS_IOCTL_DEV
+#define LANDLOCK_LAST_ACCESS_FS		LANDLOCK_ACCESS_FS_RESOLVE_UNIX
 #define LANDLOCK_MASK_ACCESS_FS		((LANDLOCK_LAST_ACCESS_FS << 1) - 1)
 #define LANDLOCK_NUM_ACCESS_FS		__const_hweight64(LANDLOCK_MASK_ACCESS_FS)
 
security/landlock/syscalls.c +1 -1
index 0d66a68677b7..933902d43241 100644
@@ -164,7 +164,7 @@ static const struct file_operations ruleset_fops = {
  * If the change involves a fix that requires userspace awareness, also update
  * the errata documentation in Documentation/userspace-api/landlock.rst .
  */
-const int landlock_abi_version = 8;
+const int landlock_abi_version = 9;
 
 /**
  * sys_landlock_create_ruleset - Create a new ruleset
tools/testing/selftests/landlock/base_test.c +1 -1
index 0fea236ef4bd..30d37234086c 100644
@@ -76,7 +76,7 @@ TEST(abi_version)
 	const struct landlock_ruleset_attr ruleset_attr = {
 		.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
 	};
-	ASSERT_EQ(8, landlock_create_ruleset(NULL, 0,
+	ASSERT_EQ(9, landlock_create_ruleset(NULL, 0,
 					     LANDLOCK_CREATE_RULESET_VERSION));
 
 	ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
tools/testing/selftests/landlock/fs_test.c +3 -2
index 968a91c927a4..b318627e7561 100644
@@ -575,9 +575,10 @@ TEST_F_FORK(layout1, inval)
 	LANDLOCK_ACCESS_FS_WRITE_FILE | \
 	LANDLOCK_ACCESS_FS_READ_FILE | \
 	LANDLOCK_ACCESS_FS_TRUNCATE | \
-	LANDLOCK_ACCESS_FS_IOCTL_DEV)
+	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
 
-#define ACCESS_LAST LANDLOCK_ACCESS_FS_IOCTL_DEV
+#define ACCESS_LAST LANDLOCK_ACCESS_FS_RESOLVE_UNIX
 
 #define ACCESS_ALL ( \
 	ACCESS_FILE | \
Signature · 1 line
2.52.0
maintainer
#15 lore raw

Re: [PATCH v5 2/9] landlock: Control pathname UNIX domain socket resolution by path

Quoted 79 lines · Mickaël Salaün
Quoted 78 lines · Günther Noack

* Add a new access right LANDLOCK_ACCESS_FS_RESOLVE_UNIX, which controls the look up operations for named UNIX domain sockets. The resolution happens during connect() and sendmsg() (depending on socket type).
* Hook into the path lookup in unix_find_bsd() in af_unix.c, using a LSM hook. Make policy decisions based on the new access rights
* Increment the Landlock ABI version.
* Minor test adaptions to keep the tests working.

With this access right, access is granted if either of the following conditions is met:

* The target socket's filesystem path was allow-listed using a LANDLOCK_RULE_PATH_BENEATH rule, *or*:
* The target socket was created in the same Landlock domain in which LANDLOCK_ACCESS_FS_RESOLVE_UNIX was restricted.

In case of a denial, connect() and sendmsg() return EACCES, which is the same error as it is returned if the user does not have the write bit in the traditional Unix file system permissions of that file.

This feature was created with substantial discussion and input from Justin Suess, Tingmao Wang and Mickaël Salaün.

---
 include/uapi/linux/landlock.h                |  10 ++
 security/landlock/access.h                   |  11 +-
 security/landlock/audit.c                    |   1 +
 security/landlock/fs.c                       | 102 ++++++++++++++++++-
 security/landlock/limits.h                   |   2 +-
 security/landlock/syscalls.c                 |   2 +-
 tools/testing/selftests/landlock/base_test.c |   2 +-
 tools/testing/selftests/landlock/fs_test.c   |   5 +-
 8 files changed, 128 insertions(+), 7 deletions(-)

index 60ff217ab95b..8d0edf94037d 100644

security/landlock/audit.c +1 -0
@@ -37,6 +37,7 @@ static const char *const fs_access_strings[] = {
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev",
+	[BIT_INDEX(LANDLOCK_ACCESS_FS_RESOLVE_UNIX)] = "fs.resolve_unix",
 };
 
 static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);
security/landlock/fs.c +3 -1
index e764470f588c..76035c6f2bf1 100644
@@ -27,6 +27,7 @@
 #include <linux/lsm_hooks.h>
 #include <linux/mount.h>
 #include <linux/namei.h>
+#include <linux/net.h>
 #include <linux/path.h>
 #include <linux/pid.h>
 #include <linux/rcupdate.h>
@@ -314,7 +315,8 @@ static struct landlock_object *get_inode_object(struct inode *const inode)
 	LANDLOCK_ACCESS_FS_WRITE_FILE | \
 	LANDLOCK_ACCESS_FS_READ_FILE | \
 	LANDLOCK_ACCESS_FS_TRUNCATE | \
-	LANDLOCK_ACCESS_FS_IOCTL_DEV)
+	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
 /* clang-format on */
 
 /*
@@ -1561,6 +1563,103 @@ static int hook_path_truncate(const struct path *const path)
 	return current_check_access_path(path, LANDLOCK_ACCESS_FS_TRUNCATE);
 }
 
Quoted 6 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1571

Quoted 6 lines
security/landlock/fs.c +6 -0
+/**
+ * unmask_scoped_access - Remove access right bits in @masks in all layers
+ *                        where @client and @server have the same domain
+ *
+ * This does the same as domain_is_scoped(), but unmasks bits in @masks.
+ * It can not return early as domain_is_scoped() does.

Why can't we use the same logic as for other scopes?

Quoted 5 lines
Quoted 5 lines
security/landlock/fs.c +5 -0
+ *
+ * @client: Client domain
+ * @server: Server domain
+ * @masks: Layer access masks to unmask
+ * @access: Access bit that controls scoping
Quoted 8 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1581

Quoted 5 lines
security/landlock/fs.c +5 -0
+ */
+static void unmask_scoped_access(const struct landlock_ruleset *const client,
+				 const struct landlock_ruleset *const server,
+				 struct layer_access_masks *const masks,
+				 const access_mask_t access)

This helper should be moved to task.c and factored out with domain_is_scoped(). This should be a dedicated patch.

Well, if domain_is_scoped() can be refactored and made generic, it would make more sense to move it to domain.c

Quoted 37 lines
Quoted 37 lines
security/landlock/fs.c +36 -0
+{
+	int client_layer, server_layer;
+	const struct landlock_hierarchy *client_walker, *server_walker;
+
+	if (WARN_ON_ONCE(!client))
+		return; /* should not happen */
+
+	if (!server)
+		return; /* server has no Landlock domain; nothing to clear */
+
+	client_layer = client->num_layers - 1;
+	client_walker = client->hierarchy;
+	server_layer = server->num_layers - 1;
+	server_walker = server->hierarchy;
+
+	/*
+	 * Clears the access bits at all layers where the client domain is the
+	 * same as the server domain.  We start the walk at min(client_layer,
+	 * server_layer).  The layer bits until there can not be cleared because
+	 * either the client or the server domain is missing.
+	 */
+	for (; client_layer > server_layer; client_layer--)
+		client_walker = client_walker->parent;
+
+	for (; server_layer > client_layer; server_layer--)
+		server_walker = server_walker->parent;
+
+	for (; client_layer >= 0; client_layer--) {
+		if (masks->access[client_layer] & access &&
+		    client_walker == server_walker)
+			masks->access[client_layer] &= ~access;
+
+		client_walker = client_walker->parent;
+		server_walker = server_walker->parent;
+	}
+}
#16 lore raw

Re: [PATCH v5 2/9] landlock: Control pathname UNIX domain socket resolution by path

Quoted 80 lines · Mickaël Salaün
Quoted 79 lines · Mickaël Salaün
Quoted 78 lines · Günther Noack

* Add a new access right LANDLOCK_ACCESS_FS_RESOLVE_UNIX, which controls the look up operations for named UNIX domain sockets. The resolution happens during connect() and sendmsg() (depending on socket type).
* Hook into the path lookup in unix_find_bsd() in af_unix.c, using a LSM hook. Make policy decisions based on the new access rights
* Increment the Landlock ABI version.
* Minor test adaptions to keep the tests working.

With this access right, access is granted if either of the following conditions is met:

* The target socket's filesystem path was allow-listed using a LANDLOCK_RULE_PATH_BENEATH rule, *or*:
* The target socket was created in the same Landlock domain in which LANDLOCK_ACCESS_FS_RESOLVE_UNIX was restricted.

In case of a denial, connect() and sendmsg() return EACCES, which is the same error as it is returned if the user does not have the write bit in the traditional Unix file system permissions of that file.

This feature was created with substantial discussion and input from Justin Suess, Tingmao Wang and Mickaël Salaün.

---
 include/uapi/linux/landlock.h                |  10 ++
 security/landlock/access.h                   |  11 +-
 security/landlock/audit.c                    |   1 +
 security/landlock/fs.c                       | 102 ++++++++++++++++++-
 security/landlock/limits.h                   |   2 +-
 security/landlock/syscalls.c                 |   2 +-
 tools/testing/selftests/landlock/base_test.c |   2 +-
 tools/testing/selftests/landlock/fs_test.c   |   5 +-
 8 files changed, 128 insertions(+), 7 deletions(-)

index 60ff217ab95b..8d0edf94037d 100644

security/landlock/audit.c +1 -0
@@ -37,6 +37,7 @@ static const char *const fs_access_strings[] = {
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev",
+	[BIT_INDEX(LANDLOCK_ACCESS_FS_RESOLVE_UNIX)] = "fs.resolve_unix",
 };
 
 static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);
security/landlock/fs.c +3 -1
index e764470f588c..76035c6f2bf1 100644
@@ -27,6 +27,7 @@
 #include <linux/lsm_hooks.h>
 #include <linux/mount.h>
 #include <linux/namei.h>
+#include <linux/net.h>
 #include <linux/path.h>
 #include <linux/pid.h>
 #include <linux/rcupdate.h>
@@ -314,7 +315,8 @@ static struct landlock_object *get_inode_object(struct inode *const inode)
 	LANDLOCK_ACCESS_FS_WRITE_FILE | \
 	LANDLOCK_ACCESS_FS_READ_FILE | \
 	LANDLOCK_ACCESS_FS_TRUNCATE | \
-	LANDLOCK_ACCESS_FS_IOCTL_DEV)
+	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
 /* clang-format on */
 
 /*
@@ -1561,6 +1563,103 @@ static int hook_path_truncate(const struct path *const path)
 	return current_check_access_path(path, LANDLOCK_ACCESS_FS_TRUNCATE);
 }
 
Quoted 8 lines
Quoted 6 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1571

Quoted 6 lines
security/landlock/fs.c +6 -0
+/**
+ * unmask_scoped_access - Remove access right bits in @masks in all layers
+ *                        where @client and @server have the same domain
+ *
+ * This does the same as domain_is_scoped(), but unmasks bits in @masks.
+ * It can not return early as domain_is_scoped() does.

Why can't we use the same logic as for other scopes?

The other scopes, for which this is implemented in domain_is_scoped(), do not need to do this layer-by-layer.

I have to admit, in my initial implementation, I was using domain_is_scoped() directly, and the logic at the end of the hook was roughly:

--- BUGGY CODE START ---
// ...

if (!domain_is_scoped(..., ..., LANDLOCK_ACCESS_FS_RESOLVE_UNIX))
return 0; /* permitted */

return current_check_access_path(path, LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
}
--- BUGGY CODE END ---

Unfortunately, that is a logic error though -- it implements the formula

Access granted if:
(FOR-ALL l ∈ layers scoped-access-ok(l)) OR (FOR-ALL l ∈ layers path-access-ok(l)) (WRONG!)

but the formula we want is:

Access granted if:
FOR-ALL l ∈ layers (scoped-access-ok(l) OR path-access-ok(l)) (CORRECT!)

This makes a difference in the case where (pseudocode):

1. landlock_restrict_self(RESOLVE_UNIX) // d1
2. create_unix_server("./sock")
3. landlock_restrict_self(RESOLVE_UNIX, rule=Allow(".", RESOLVE_UNIX)) // d2
4. connect_unix("./sock")

,------------------------------------------------d1--,

   |                                                    |
   |    ./sock server                                   |
   |       ^                                            |
   |       |                                            |
   |  ,------------------------------------------d2--,  |
   |  |    |                                         |  |
   |  |  client                                      |  |
   |  |                                              |  |
   |  '----------------------------------------------'  |
   |                                                    |

'----------------------------------------------------'

(BTW, this scenario is covered in the selftests, that is why there is a variant of these selftests where instead of applying "no domain", we apply a domain with an exception rule like in step 3 in the pseudocode above. Applying that domain should behave the same as applying no domain at all.)

Intuitively, it is clear that the access should be granted:

- d1 does not restrict access to the server, because the socket was created within d1 itself.
- d2 does not restrict access to the server, because it has a rule to allow it

But the "buggy code" logic above comes to a different conclusion:

- the domain_is_scoped() check denies the access, because the server is in a more privileged domain relative to the client domain.
- the current_check_access_path() check denies the access as well, because the socket's path is not allow-listed in d1.

In the 'intuitive' reasoning above, we are checking d1 and d2 independently of each other. While Landlock is not implemented like that internally, we need to stay consistent with it so that domains compose correctly. The way to do that is to track is access check results on a per-layer basis again, and that is why unmask_scoped_access() uses a layer mask for tracking. The original domain_is_scoped() does not use a layer mask, but that also means that it can return early in some scenarios -- if for any of the relevant layer depths, the client and server domains are not the same, it exits early with failure because it's overall not fulfillable any more. In the RESOLVE_UNIX case though, we need to remember in which layers we failed (both high an low ones), because these layers can still be fulfilled with a PATH_BENEATH rule later.

Summary:

Option 1: We *can* unify this if you want. It just might come at a small performance penalty for domain_is_scoped(), which now uses the larger layer mask data structure and can't do the same early returns any more as before.

Option 2: Alternatively, if we move the two functions into the same module, we can keep them separate but still test them against each other to make sure they are in-line:

This invocation should return true...

domain_is_scoped(cli, srv, access)

...in the exactly the same situations where this invocation leaves any bits set in layer_masks:

  landlock_init_layer_masks(dom, access, &layer_masks, LL_KEY_INODE);
  unmask_scoped_access(cli, srv, &layer_masks, access);

What do you prefer?

Quoted 54 lines
Quoted 5 lines
Quoted 5 lines
security/landlock/fs.c +5 -0
+ *
+ * @client: Client domain
+ * @server: Server domain
+ * @masks: Layer access masks to unmask
+ * @access: Access bit that controls scoping
Quoted 8 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1581

Quoted 5 lines
security/landlock/fs.c +5 -0
+ */
+static void unmask_scoped_access(const struct landlock_ruleset *const client,
+				 const struct landlock_ruleset *const server,
+				 struct layer_access_masks *const masks,
+				 const access_mask_t access)

This helper should be moved to task.c and factored out with domain_is_scoped(). This should be a dedicated patch.

Well, if domain_is_scoped() can be refactored and made generic, it would make more sense to move it to domain.c

Quoted 37 lines
Quoted 37 lines
security/landlock/fs.c +36 -0
+{
+	int client_layer, server_layer;
+	const struct landlock_hierarchy *client_walker, *server_walker;
+
+	if (WARN_ON_ONCE(!client))
+		return; /* should not happen */
+
+	if (!server)
+		return; /* server has no Landlock domain; nothing to clear */
+
+	client_layer = client->num_layers - 1;
+	client_walker = client->hierarchy;
+	server_layer = server->num_layers - 1;
+	server_walker = server->hierarchy;
+
+	/*
+	 * Clears the access bits at all layers where the client domain is the
+	 * same as the server domain.  We start the walk at min(client_layer,
+	 * server_layer).  The layer bits until there can not be cleared because
+	 * either the client or the server domain is missing.
+	 */
+	for (; client_layer > server_layer; client_layer--)
+		client_walker = client_walker->parent;
+
+	for (; server_layer > client_layer; server_layer--)
+		server_walker = server_walker->parent;
+
+	for (; client_layer >= 0; client_layer--) {
+		if (masks->access[client_layer] & access &&
+		    client_walker == server_walker)
+			masks->access[client_layer] &= ~access;
+
+		client_walker = client_walker->parent;
+		server_walker = server_walker->parent;
+	}
+}
maintainer
#17 lore raw

Re: [PATCH v5 2/9] landlock: Control pathname UNIX domain socket resolution by path

Quoted 109 lines · Günther Noack
Quoted 80 lines · Mickaël Salaün
Quoted 79 lines · Mickaël Salaün
Quoted 78 lines · Günther Noack

* Add a new access right LANDLOCK_ACCESS_FS_RESOLVE_UNIX, which controls the look up operations for named UNIX domain sockets. The resolution happens during connect() and sendmsg() (depending on socket type).
* Hook into the path lookup in unix_find_bsd() in af_unix.c, using a LSM hook. Make policy decisions based on the new access rights
* Increment the Landlock ABI version.
* Minor test adaptions to keep the tests working.

With this access right, access is granted if either of the following conditions is met:

* The target socket's filesystem path was allow-listed using a LANDLOCK_RULE_PATH_BENEATH rule, *or*:
* The target socket was created in the same Landlock domain in which LANDLOCK_ACCESS_FS_RESOLVE_UNIX was restricted.

In case of a denial, connect() and sendmsg() return EACCES, which is the same error as it is returned if the user does not have the write bit in the traditional Unix file system permissions of that file.

This feature was created with substantial discussion and input from Justin Suess, Tingmao Wang and Mickaël Salaün.

---
 include/uapi/linux/landlock.h                |  10 ++
 security/landlock/access.h                   |  11 +-
 security/landlock/audit.c                    |   1 +
 security/landlock/fs.c                       | 102 ++++++++++++++++++-
 security/landlock/limits.h                   |   2 +-
 security/landlock/syscalls.c                 |   2 +-
 tools/testing/selftests/landlock/base_test.c |   2 +-
 tools/testing/selftests/landlock/fs_test.c   |   5 +-
 8 files changed, 128 insertions(+), 7 deletions(-)

index 60ff217ab95b..8d0edf94037d 100644

security/landlock/audit.c +1 -0
@@ -37,6 +37,7 @@ static const char *const fs_access_strings[] = {
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev",
+	[BIT_INDEX(LANDLOCK_ACCESS_FS_RESOLVE_UNIX)] = "fs.resolve_unix",
 };
 
 static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);
security/landlock/fs.c +3 -1
index e764470f588c..76035c6f2bf1 100644
@@ -27,6 +27,7 @@
 #include <linux/lsm_hooks.h>
 #include <linux/mount.h>
 #include <linux/namei.h>
+#include <linux/net.h>
 #include <linux/path.h>
 #include <linux/pid.h>
 #include <linux/rcupdate.h>
@@ -314,7 +315,8 @@ static struct landlock_object *get_inode_object(struct inode *const inode)
 	LANDLOCK_ACCESS_FS_WRITE_FILE | \
 	LANDLOCK_ACCESS_FS_READ_FILE | \
 	LANDLOCK_ACCESS_FS_TRUNCATE | \
-	LANDLOCK_ACCESS_FS_IOCTL_DEV)
+	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
 /* clang-format on */
 
 /*
@@ -1561,6 +1563,103 @@ static int hook_path_truncate(const struct path *const path)
 	return current_check_access_path(path, LANDLOCK_ACCESS_FS_TRUNCATE);
 }
 
Quoted 8 lines
Quoted 6 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1571

Quoted 6 lines
security/landlock/fs.c +6 -0
+/**
+ * unmask_scoped_access - Remove access right bits in @masks in all layers
+ *                        where @client and @server have the same domain
+ *
+ * This does the same as domain_is_scoped(), but unmasks bits in @masks.
+ * It can not return early as domain_is_scoped() does.

Why can't we use the same logic as for other scopes?

The other scopes, for which this is implemented in domain_is_scoped(), do not need to do this layer-by-layer.

I have to admit, in my initial implementation, I was using domain_is_scoped() directly, and the logic at the end of the hook was roughly:

   --- BUGGY CODE START ---
       // ...
       
       if (!domain_is_scoped(..., ..., LANDLOCK_ACCESS_FS_RESOLVE_UNIX))
           return 0;  /* permitted */

       return current_check_access_path(path, LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
   }
   --- BUGGY CODE END ---

Unfortunately, that is a logic error though -- it implements the formula

Quoted 7 lines

Access granted if:
(FOR-ALL l ∈ layers scoped-access-ok(l)) OR (FOR-ALL l ∈ layers path-access-ok(l)) (WRONG!)

but the formula we want is:

Access granted if:
FOR-ALL l ∈ layers (scoped-access-ok(l) OR path-access-ok(l)) (CORRECT!)

It is worth it to add this explanation to the unmask_scoped_access() description, also pointing to the test that check this case.

Quoted 70 lines

This makes a difference in the case where (pseudocode):

1. landlock_restrict_self(RESOLVE_UNIX) // d1
2. create_unix_server("./sock")
3. landlock_restrict_self(RESOLVE_UNIX, rule=Allow(".", RESOLVE_UNIX)) // d2
4. connect_unix("./sock")

,------------------------------------------------d1--,

   |                                                    |
   |    ./sock server                                   |
   |       ^                                            |
   |       |                                            |
   |  ,------------------------------------------d2--,  |
   |  |    |                                         |  |
   |  |  client                                      |  |
   |  |                                              |  |
   |  '----------------------------------------------'  |
   |                                                    |

'----------------------------------------------------'

(BTW, this scenario is covered in the selftests, that is why there is a variant of these selftests where instead of applying "no domain", we apply a domain with an exception rule like in step 3 in the pseudocode above. Applying that domain should behave the same as applying no domain at all.)

Intuitively, it is clear that the access should be granted:

- d1 does not restrict access to the server, because the socket was created within d1 itself.
- d2 does not restrict access to the server, because it has a rule to allow it

But the "buggy code" logic above comes to a different conclusion:

- the domain_is_scoped() check denies the access, because the server is in a more privileged domain relative to the client domain.
- the current_check_access_path() check denies the access as well, because the socket's path is not allow-listed in d1.

In the 'intuitive' reasoning above, we are checking d1 and d2 independently of each other. While Landlock is not implemented like that internally, we need to stay consistent with it so that domains compose correctly. The way to do that is to track is access check results on a per-layer basis again, and that is why unmask_scoped_access() uses a layer mask for tracking. The original domain_is_scoped() does not use a layer mask, but that also means that it can return early in some scenarios -- if for any of the relevant layer depths, the client and server domains are not the same, it exits early with failure because it's overall not fulfillable any more. In the RESOLVE_UNIX case though, we need to remember in which layers we failed (both high an low ones), because these layers can still be fulfilled with a PATH_BENEATH rule later.

Summary:

Option 1: We *can* unify this if you want. It just might come at a small performance penalty for domain_is_scoped(), which now uses the larger layer mask data structure and can't do the same early returns any more as before.

Option 2: Alternatively, if we move the two functions into the same module, we can keep them separate but still test them against each other to make sure they are in-line:

This invocation should return true...

domain_is_scoped(cli, srv, access)

Quoted 7 lines

...in the exactly the same situations where this invocation leaves any bits set in layer_masks:

  landlock_init_layer_masks(dom, access, &layer_masks, LL_KEY_INODE);
  unmask_scoped_access(cli, srv, &layer_masks, access);

What do you prefer?

I was thinking about factoring out domain_is_scoped() with unmask_scoped_access() but, after some tests, it is not worth it. Your approach is simple and good.

Quoted 20 lines
Quoted 20 lines
Quoted 15 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1581

Quoted 12 lines
security/landlock/fs.c +10 -0
+ *
+ * @client: Client domain
+ * @server: Server domain
+ * @masks: Layer access masks to unmask
+ * @access: Access bit that controls scoping
+ */
+static void unmask_scoped_access(const struct landlock_ruleset *const client,
+				 const struct landlock_ruleset *const server,
+				 struct layer_access_masks *const masks,
+				 const access_mask_t access)

This helper should be moved to task.c and factored out with domain_is_scoped(). This should be a dedicated patch.

Well, if domain_is_scoped() can be refactored and made generic, it would make more sense to move it to domain.c

Quoted 1 line
Quoted 6 lines
Quoted 6 lines
Quoted 6 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1587

Quoted 6 lines
security/landlock/fs.c +6 -0
+{
+	int client_layer, server_layer;
+	const struct landlock_hierarchy *client_walker, *server_walker;
+
+	if (WARN_ON_ONCE(!client))
+		return; /* should not happen */

Please no comment after ";"

Quoted 16 lines
Quoted 16 lines
Quoted 16 lines
Quoted 16 lines
security/landlock/fs.c +16 -0
+
+	if (!server)
+		return; /* server has no Landlock domain; nothing to clear */
+
+	client_layer = client->num_layers - 1;
+	client_walker = client->hierarchy;
+	server_layer = server->num_layers - 1;
+	server_walker = server->hierarchy;
+
+	/*
+	 * Clears the access bits at all layers where the client domain is the
+	 * same as the server domain.  We start the walk at min(client_layer,
+	 * server_layer).  The layer bits until there can not be cleared because
+	 * either the client or the server domain is missing.
+	 */
+	for (; client_layer > server_layer; client_layer--)
Quoted 8 lines
Quoted 8 lines
Quoted 8 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1611

Quoted 8 lines
security/landlock/fs.c +8 -0
+		client_walker = client_walker->parent;
+
+	for (; server_layer > client_layer; server_layer--)
+		server_walker = server_walker->parent;
+
+	for (; client_layer >= 0; client_layer--) {
+		if (masks->access[client_layer] & access &&
+		    client_walker == server_walker)

I'd prefer to first check client_walker == server_walker and then the access. My main concern is that only one bit of access matching masks->access[client_layer] clear all the access request bits. In practice there is only one, for now, but this code should be more strict by following a defensive approach.

Quoted 7 lines
Quoted 6 lines
Quoted 6 lines
Quoted 6 lines
security/landlock/fs.c +6 -0
+			masks->access[client_layer] &= ~access;
+
+		client_walker = client_walker->parent;
+		server_walker = server_walker->parent;
+	}
+}
maintainer
#18 lore raw

Re: [PATCH v5 2/9] landlock: Control pathname UNIX domain socket resolution by path

Quoted 264 lines · Mickaël Salaün
Quoted 109 lines · Günther Noack
Quoted 80 lines · Mickaël Salaün
Quoted 79 lines · Mickaël Salaün
Quoted 78 lines · Günther Noack

* Add a new access right LANDLOCK_ACCESS_FS_RESOLVE_UNIX, which controls the look up operations for named UNIX domain sockets. The resolution happens during connect() and sendmsg() (depending on socket type).
* Hook into the path lookup in unix_find_bsd() in af_unix.c, using a LSM hook. Make policy decisions based on the new access rights
* Increment the Landlock ABI version.
* Minor test adaptions to keep the tests working.

With this access right, access is granted if either of the following conditions is met:

* The target socket's filesystem path was allow-listed using a LANDLOCK_RULE_PATH_BENEATH rule, *or*:
* The target socket was created in the same Landlock domain in which LANDLOCK_ACCESS_FS_RESOLVE_UNIX was restricted.

In case of a denial, connect() and sendmsg() return EACCES, which is the same error as it is returned if the user does not have the write bit in the traditional Unix file system permissions of that file.

This feature was created with substantial discussion and input from Justin Suess, Tingmao Wang and Mickaël Salaün.

---
 include/uapi/linux/landlock.h                |  10 ++
 security/landlock/access.h                   |  11 +-
 security/landlock/audit.c                    |   1 +
 security/landlock/fs.c                       | 102 ++++++++++++++++++-
 security/landlock/limits.h                   |   2 +-
 security/landlock/syscalls.c                 |   2 +-
 tools/testing/selftests/landlock/base_test.c |   2 +-
 tools/testing/selftests/landlock/fs_test.c   |   5 +-
 8 files changed, 128 insertions(+), 7 deletions(-)

index 60ff217ab95b..8d0edf94037d 100644

security/landlock/audit.c +1 -0
@@ -37,6 +37,7 @@ static const char *const fs_access_strings[] = {
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev",
+	[BIT_INDEX(LANDLOCK_ACCESS_FS_RESOLVE_UNIX)] = "fs.resolve_unix",
 };
 
 static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);
security/landlock/fs.c +3 -1
index e764470f588c..76035c6f2bf1 100644
@@ -27,6 +27,7 @@
 #include <linux/lsm_hooks.h>
 #include <linux/mount.h>
 #include <linux/namei.h>
+#include <linux/net.h>
 #include <linux/path.h>
 #include <linux/pid.h>
 #include <linux/rcupdate.h>
@@ -314,7 +315,8 @@ static struct landlock_object *get_inode_object(struct inode *const inode)
 	LANDLOCK_ACCESS_FS_WRITE_FILE | \
 	LANDLOCK_ACCESS_FS_READ_FILE | \
 	LANDLOCK_ACCESS_FS_TRUNCATE | \
-	LANDLOCK_ACCESS_FS_IOCTL_DEV)
+	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
 /* clang-format on */
 
 /*
@@ -1561,6 +1563,103 @@ static int hook_path_truncate(const struct path *const path)
 	return current_check_access_path(path, LANDLOCK_ACCESS_FS_TRUNCATE);
 }
 
Quoted 8 lines
Quoted 6 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1571

Quoted 6 lines
security/landlock/fs.c +6 -0
+/**
+ * unmask_scoped_access - Remove access right bits in @masks in all layers
+ *                        where @client and @server have the same domain
+ *
+ * This does the same as domain_is_scoped(), but unmasks bits in @masks.
+ * It can not return early as domain_is_scoped() does.

Why can't we use the same logic as for other scopes?

The other scopes, for which this is implemented in domain_is_scoped(), do not need to do this layer-by-layer.

I have to admit, in my initial implementation, I was using domain_is_scoped() directly, and the logic at the end of the hook was roughly:

   --- BUGGY CODE START ---
       // ...
       
       if (!domain_is_scoped(..., ..., LANDLOCK_ACCESS_FS_RESOLVE_UNIX))
           return 0;  /* permitted */

       return current_check_access_path(path, LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
   }
   --- BUGGY CODE END ---

Unfortunately, that is a logic error though -- it implements the formula

Quoted 7 lines

Access granted if:
(FOR-ALL l ∈ layers scoped-access-ok(l)) OR (FOR-ALL l ∈ layers path-access-ok(l)) (WRONG!)

but the formula we want is:

Access granted if:
FOR-ALL l ∈ layers (scoped-access-ok(l) OR path-access-ok(l)) (CORRECT!)

It is worth it to add this explanation to the unmask_scoped_access() description, also pointing to the test that check this case.

Quoted 70 lines

This makes a difference in the case where (pseudocode):

1. landlock_restrict_self(RESOLVE_UNIX) // d1
2. create_unix_server("./sock")
3. landlock_restrict_self(RESOLVE_UNIX, rule=Allow(".", RESOLVE_UNIX)) // d2
4. connect_unix("./sock")

,------------------------------------------------d1--,

   |                                                    |
   |    ./sock server                                   |
   |       ^                                            |
   |       |                                            |
   |  ,------------------------------------------d2--,  |
   |  |    |                                         |  |
   |  |  client                                      |  |
   |  |                                              |  |
   |  '----------------------------------------------'  |
   |                                                    |

'----------------------------------------------------'

(BTW, this scenario is covered in the selftests, that is why there is a variant of these selftests where instead of applying "no domain", we apply a domain with an exception rule like in step 3 in the pseudocode above. Applying that domain should behave the same as applying no domain at all.)

Intuitively, it is clear that the access should be granted:

- d1 does not restrict access to the server, because the socket was created within d1 itself.
- d2 does not restrict access to the server, because it has a rule to allow it

But the "buggy code" logic above comes to a different conclusion:

- the domain_is_scoped() check denies the access, because the server is in a more privileged domain relative to the client domain.
- the current_check_access_path() check denies the access as well, because the socket's path is not allow-listed in d1.

In the 'intuitive' reasoning above, we are checking d1 and d2 independently of each other. While Landlock is not implemented like that internally, we need to stay consistent with it so that domains compose correctly. The way to do that is to track is access check results on a per-layer basis again, and that is why unmask_scoped_access() uses a layer mask for tracking. The original domain_is_scoped() does not use a layer mask, but that also means that it can return early in some scenarios -- if for any of the relevant layer depths, the client and server domains are not the same, it exits early with failure because it's overall not fulfillable any more. In the RESOLVE_UNIX case though, we need to remember in which layers we failed (both high an low ones), because these layers can still be fulfilled with a PATH_BENEATH rule later.

Summary:

Option 1: We *can* unify this if you want. It just might come at a small performance penalty for domain_is_scoped(), which now uses the larger layer mask data structure and can't do the same early returns any more as before.

Option 2: Alternatively, if we move the two functions into the same module, we can keep them separate but still test them against each other to make sure they are in-line:

This invocation should return true...

domain_is_scoped(cli, srv, access)

Quoted 7 lines

...in the exactly the same situations where this invocation leaves any bits set in layer_masks:

  landlock_init_layer_masks(dom, access, &layer_masks, LL_KEY_INODE);
  unmask_scoped_access(cli, srv, &layer_masks, access);

What do you prefer?

I was thinking about factoring out domain_is_scoped() with unmask_scoped_access() but, after some tests, it is not worth it. Your approach is simple and good.

Quoted 20 lines
Quoted 20 lines
Quoted 15 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1581

Quoted 12 lines
security/landlock/fs.c +10 -0
+ *
+ * @client: Client domain
+ * @server: Server domain
+ * @masks: Layer access masks to unmask
+ * @access: Access bit that controls scoping
+ */
+static void unmask_scoped_access(const struct landlock_ruleset *const client,
+				 const struct landlock_ruleset *const server,
+				 struct layer_access_masks *const masks,
+				 const access_mask_t access)

This helper should be moved to task.c and factored out with domain_is_scoped(). This should be a dedicated patch.

Well, if domain_is_scoped() can be refactored and made generic, it would make more sense to move it to domain.c

Quoted 1 line
Quoted 6 lines
Quoted 6 lines
Quoted 6 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1587

Quoted 6 lines
security/landlock/fs.c +6 -0
+{
+	int client_layer, server_layer;
+	const struct landlock_hierarchy *client_walker, *server_walker;
+
+	if (WARN_ON_ONCE(!client))
+		return; /* should not happen */

Please no comment after ";"

Quoted 16 lines
Quoted 16 lines
Quoted 16 lines
Quoted 16 lines
security/landlock/fs.c +16 -0
+
+	if (!server)
+		return; /* server has no Landlock domain; nothing to clear */
+
+	client_layer = client->num_layers - 1;
+	client_walker = client->hierarchy;
+	server_layer = server->num_layers - 1;
+	server_walker = server->hierarchy;
+
+	/*
+	 * Clears the access bits at all layers where the client domain is the
+	 * same as the server domain.  We start the walk at min(client_layer,
+	 * server_layer).  The layer bits until there can not be cleared because
+	 * either the client or the server domain is missing.
+	 */
+	for (; client_layer > server_layer; client_layer--)
Quoted 8 lines
Quoted 8 lines
Quoted 8 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1611

Quoted 8 lines
security/landlock/fs.c +8 -0
+		client_walker = client_walker->parent;
+
+	for (; server_layer > client_layer; server_layer--)
+		server_walker = server_walker->parent;
+
+	for (; client_layer >= 0; client_layer--) {
+		if (masks->access[client_layer] & access &&
+		    client_walker == server_walker)

I'd prefer to first check client_walker == server_walker and then the access. My main concern is that only one bit of access matching masks->access[client_layer] clear all the access request bits. In practice there is only one, for now, but this code should be more strict by following a defensive approach.

Quoted 1 line
Quoted 1 line
Quoted 1 line
Quoted 1 line

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1612

Quoted 1 line
security/landlock/fs.c +1 -0
+			masks->access[client_layer] &= ~access;

Actually, why not removing the access argument and just reset masks->access[client_layer]? The doc would need some updates.

Quoted 6 lines
Quoted 6 lines
Quoted 5 lines
Quoted 5 lines
Quoted 5 lines
security/landlock/fs.c +5 -0
+
+		client_walker = client_walker->parent;
+		server_walker = server_walker->parent;
+	}
+}
#19 lore raw

Re: [PATCH v5 2/9] landlock: Control pathname UNIX domain socket resolution by path

Quoted 255 lines · Mickaël Salaün
Quoted 254 lines · Mickaël Salaün
Quoted 109 lines · Günther Noack
Quoted 80 lines · Mickaël Salaün
Quoted 79 lines · Mickaël Salaün
Quoted 78 lines · Günther Noack

* Add a new access right LANDLOCK_ACCESS_FS_RESOLVE_UNIX, which controls the look up operations for named UNIX domain sockets. The resolution happens during connect() and sendmsg() (depending on socket type).
* Hook into the path lookup in unix_find_bsd() in af_unix.c, using a LSM hook. Make policy decisions based on the new access rights
* Increment the Landlock ABI version.
* Minor test adaptions to keep the tests working.

With this access right, access is granted if either of the following conditions is met:

* The target socket's filesystem path was allow-listed using a LANDLOCK_RULE_PATH_BENEATH rule, *or*:
* The target socket was created in the same Landlock domain in which LANDLOCK_ACCESS_FS_RESOLVE_UNIX was restricted.

In case of a denial, connect() and sendmsg() return EACCES, which is the same error as it is returned if the user does not have the write bit in the traditional Unix file system permissions of that file.

This feature was created with substantial discussion and input from Justin Suess, Tingmao Wang and Mickaël Salaün.

---
 include/uapi/linux/landlock.h                |  10 ++
 security/landlock/access.h                   |  11 +-
 security/landlock/audit.c                    |   1 +
 security/landlock/fs.c                       | 102 ++++++++++++++++++-
 security/landlock/limits.h                   |   2 +-
 security/landlock/syscalls.c                 |   2 +-
 tools/testing/selftests/landlock/base_test.c |   2 +-
 tools/testing/selftests/landlock/fs_test.c   |   5 +-
 8 files changed, 128 insertions(+), 7 deletions(-)

index 60ff217ab95b..8d0edf94037d 100644

security/landlock/audit.c +1 -0
@@ -37,6 +37,7 @@ static const char *const fs_access_strings[] = {
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev",
+	[BIT_INDEX(LANDLOCK_ACCESS_FS_RESOLVE_UNIX)] = "fs.resolve_unix",
 };
 
 static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);
security/landlock/fs.c +3 -1
index e764470f588c..76035c6f2bf1 100644
@@ -27,6 +27,7 @@
 #include <linux/lsm_hooks.h>
 #include <linux/mount.h>
 #include <linux/namei.h>
+#include <linux/net.h>
 #include <linux/path.h>
 #include <linux/pid.h>
 #include <linux/rcupdate.h>
@@ -314,7 +315,8 @@ static struct landlock_object *get_inode_object(struct inode *const inode)
 	LANDLOCK_ACCESS_FS_WRITE_FILE | \
 	LANDLOCK_ACCESS_FS_READ_FILE | \
 	LANDLOCK_ACCESS_FS_TRUNCATE | \
-	LANDLOCK_ACCESS_FS_IOCTL_DEV)
+	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
 /* clang-format on */
 
 /*
@@ -1561,6 +1563,103 @@ static int hook_path_truncate(const struct path *const path)
 	return current_check_access_path(path, LANDLOCK_ACCESS_FS_TRUNCATE);
 }
 
Quoted 8 lines
Quoted 6 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1571

Quoted 6 lines
security/landlock/fs.c +6 -0
+/**
+ * unmask_scoped_access - Remove access right bits in @masks in all layers
+ *                        where @client and @server have the same domain
+ *
+ * This does the same as domain_is_scoped(), but unmasks bits in @masks.
+ * It can not return early as domain_is_scoped() does.

Why can't we use the same logic as for other scopes?

The other scopes, for which this is implemented in domain_is_scoped(), do not need to do this layer-by-layer.

I have to admit, in my initial implementation, I was using domain_is_scoped() directly, and the logic at the end of the hook was roughly:

   --- BUGGY CODE START ---
       // ...
       
       if (!domain_is_scoped(..., ..., LANDLOCK_ACCESS_FS_RESOLVE_UNIX))
           return 0;  /* permitted */

       return current_check_access_path(path, LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
   }
   --- BUGGY CODE END ---

Unfortunately, that is a logic error though -- it implements the formula

Quoted 7 lines

Access granted if:
(FOR-ALL l ∈ layers scoped-access-ok(l)) OR (FOR-ALL l ∈ layers path-access-ok(l)) (WRONG!)

but the formula we want is:

Access granted if:
FOR-ALL l ∈ layers (scoped-access-ok(l) OR path-access-ok(l)) (CORRECT!)

It is worth it to add this explanation to the unmask_scoped_access() description, also pointing to the test that check this case.

Quoted 70 lines

This makes a difference in the case where (pseudocode):

1. landlock_restrict_self(RESOLVE_UNIX) // d1
2. create_unix_server("./sock")
3. landlock_restrict_self(RESOLVE_UNIX, rule=Allow(".", RESOLVE_UNIX)) // d2
4. connect_unix("./sock")

,------------------------------------------------d1--,

   |                                                    |
   |    ./sock server                                   |
   |       ^                                            |
   |       |                                            |
   |  ,------------------------------------------d2--,  |
   |  |    |                                         |  |
   |  |  client                                      |  |
   |  |                                              |  |
   |  '----------------------------------------------'  |
   |                                                    |

'----------------------------------------------------'

(BTW, this scenario is covered in the selftests, that is why there is a variant of these selftests where instead of applying "no domain", we apply a domain with an exception rule like in step 3 in the pseudocode above. Applying that domain should behave the same as applying no domain at all.)

Intuitively, it is clear that the access should be granted:

- d1 does not restrict access to the server, because the socket was created within d1 itself.
- d2 does not restrict access to the server, because it has a rule to allow it

But the "buggy code" logic above comes to a different conclusion:

- the domain_is_scoped() check denies the access, because the server is in a more privileged domain relative to the client domain.
- the current_check_access_path() check denies the access as well, because the socket's path is not allow-listed in d1.

In the 'intuitive' reasoning above, we are checking d1 and d2 independently of each other. While Landlock is not implemented like that internally, we need to stay consistent with it so that domains compose correctly. The way to do that is to track is access check results on a per-layer basis again, and that is why unmask_scoped_access() uses a layer mask for tracking. The original domain_is_scoped() does not use a layer mask, but that also means that it can return early in some scenarios -- if for any of the relevant layer depths, the client and server domains are not the same, it exits early with failure because it's overall not fulfillable any more. In the RESOLVE_UNIX case though, we need to remember in which layers we failed (both high an low ones), because these layers can still be fulfilled with a PATH_BENEATH rule later.

Summary:

Option 1: We *can* unify this if you want. It just might come at a small performance penalty for domain_is_scoped(), which now uses the larger layer mask data structure and can't do the same early returns any more as before.

Option 2: Alternatively, if we move the two functions into the same module, we can keep them separate but still test them against each other to make sure they are in-line:

This invocation should return true...

domain_is_scoped(cli, srv, access)

Quoted 7 lines

...in the exactly the same situations where this invocation leaves any bits set in layer_masks:

  landlock_init_layer_masks(dom, access, &layer_masks, LL_KEY_INODE);
  unmask_scoped_access(cli, srv, &layer_masks, access);

What do you prefer?

I was thinking about factoring out domain_is_scoped() with unmask_scoped_access() but, after some tests, it is not worth it. Your approach is simple and good.

Quoted 20 lines
Quoted 20 lines
Quoted 15 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1581

Quoted 12 lines
security/landlock/fs.c +10 -0
+ *
+ * @client: Client domain
+ * @server: Server domain
+ * @masks: Layer access masks to unmask
+ * @access: Access bit that controls scoping
+ */
+static void unmask_scoped_access(const struct landlock_ruleset *const client,
+				 const struct landlock_ruleset *const server,
+				 struct layer_access_masks *const masks,
+				 const access_mask_t access)

This helper should be moved to task.c and factored out with domain_is_scoped(). This should be a dedicated patch.

Well, if domain_is_scoped() can be refactored and made generic, it would make more sense to move it to domain.c

Quoted 1 line
Quoted 6 lines
Quoted 6 lines
Quoted 6 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1587

Quoted 6 lines
security/landlock/fs.c +6 -0
+{
+	int client_layer, server_layer;
+	const struct landlock_hierarchy *client_walker, *server_walker;
+
+	if (WARN_ON_ONCE(!client))
+		return; /* should not happen */

Please no comment after ";"

Quoted 22 lines
Quoted 22 lines
Quoted 22 lines
Quoted 22 lines
security/landlock/fs.c +22 -0
+
+	if (!server)
+		return; /* server has no Landlock domain; nothing to clear */
+
+	client_layer = client->num_layers - 1;
+	client_walker = client->hierarchy;
+	server_layer = server->num_layers - 1;
+	server_walker = server->hierarchy;
+
+	/*
+	 * Clears the access bits at all layers where the client domain is the
+	 * same as the server domain.  We start the walk at min(client_layer,
+	 * server_layer).  The layer bits until there can not be cleared because
+	 * either the client or the server domain is missing.
+	 */
+	for (; client_layer > server_layer; client_layer--)
+		client_walker = client_walker->parent;
+
+	for (; server_layer > client_layer; server_layer--)
+		server_walker = server_walker->parent;
+
+	for (; client_layer >= 0; client_layer--) {
Quoted 8 lines
Quoted 8 lines
Quoted 2 lines
Quoted 2 lines
Quoted 2 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1611

Quoted 2 lines
security/landlock/fs.c +2 -0
+		if (masks->access[client_layer] & access &&
+		    client_walker == server_walker)

I'd prefer to first check client_walker == server_walker and then the access. My main concern is that only one bit of access matching masks->access[client_layer] clear all the access request bits. In practice there is only one, for now, but this code should be more strict by following a defensive approach.

This function works even if multiple access request bits with "scope-like" semantics were being checked in parallel; if you consider the logic in:

  if (masks->access[client_layer] & access &&
      client_walker == server_walker)
          masks->access[client_layer] &= ~access;

you'll realize that the check for "masks->access[client_layer] & access" is technically irrelevant - if that check fails, all the affected bits are already zero, so clearing them is a no-op. This code is equivalent, but might perform slightly more writes (although it likely does not make a performance difference in practice):

  if (client_walker == server_walker)
          masks->access[client_layer] &= ~access;

With that code it's a bit easier to see that "access" is actually only used to decide which bits to clear. This works both with one and with multiple access rights.

This follows the same logic as outlined in the comment above in the code, where it says:

Clears the access bits at all layers where the client domain is the same as the server domain. We start the walk at min(client_layer, server_layer). The layer bits until there can not be cleared because either the client or the server domain is missing.

Clearing bits that aren't there is a no-op

<Optional Math>

I found it helpful to visualize the scoping logic, this is directly from my notes: (Web version is at https://wiki.gnoack.org/LandlockDomainIsScoped)

The domain_is_scoped() helper implements the following predicate:

∀ l ∈ (0,16): (hasbit(self, l) implies-that domain(self, l) == domain(other, l))

That is, we require for each layer l nesting depth that:

* **If** scoping is active at the layer,
* **Then** the domains of self and other are the same at the given nesting depth.

For example:

       [ ]
        |
       [x]     self and other have the same domain at this depth
        |
       [ ]
      /   \
    [x]   [ ]  self and other have differing domains at this depth
     |     |
    [ ]   [ ]
     |
    [ ]     "other"             "x" marks a domain where "self" has
                                    set the scoping bit

"self"

</Optional Math>

Quoted 4 lines
Quoted 1 line
Quoted 1 line
Quoted 1 line
Quoted 1 line

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1612

Quoted 1 line
security/landlock/fs.c +1 -0
+			masks->access[client_layer] &= ~access;

Actually, why not removing the access argument and just reset masks->access[client_layer]? The doc would need some updates.

It would feel brittle to me if this function were to clear out unrelated access rights. It receives a struct layer_access_masks after all, where it is normally expected that multiple kinds of access rights are set. In my understanding, the bit masking does not cost much extra performance compared to clearing it out entirely, so I'd prefer to have clearer semantics and only operate on the access rights that it's about, even when the other bits are all zero at the moment.

(For full disclosure, I have contemplated for a bit whether hook_unix_find() should take a layer_mask_t-like type where each bit indicates whether a given access right (LANDLOCK_ACCESS_FS_RESOLVE_UNIX, in this case) is set at a given layer, and then it would only clear out the bits there. That would be in some ways simpler, but then the caller would still need to convert back and forth to a layer mask anyway, because that's what the other functions there take. So it didn't seem like a good option in the bigger scheme (and I would also prefer to not re-introduce layer_mask_t after we just removed it).)

Maybe I did not understand your remark fully though; Does my argument sound reasonable?

–Günther

maintainer
#20 lore raw

Re: [PATCH v5 2/9] landlock: Control pathname UNIX domain socket resolution by path

Quoted 269 lines · Günther Noack
Quoted 255 lines · Mickaël Salaün
Quoted 254 lines · Mickaël Salaün
Quoted 109 lines · Günther Noack
Quoted 80 lines · Mickaël Salaün
Quoted 79 lines · Mickaël Salaün
Quoted 78 lines · Günther Noack

* Add a new access right LANDLOCK_ACCESS_FS_RESOLVE_UNIX, which controls the look up operations for named UNIX domain sockets. The resolution happens during connect() and sendmsg() (depending on socket type).
* Hook into the path lookup in unix_find_bsd() in af_unix.c, using a LSM hook. Make policy decisions based on the new access rights
* Increment the Landlock ABI version.
* Minor test adaptions to keep the tests working.

With this access right, access is granted if either of the following conditions is met:

* The target socket's filesystem path was allow-listed using a LANDLOCK_RULE_PATH_BENEATH rule, *or*:
* The target socket was created in the same Landlock domain in which LANDLOCK_ACCESS_FS_RESOLVE_UNIX was restricted.

In case of a denial, connect() and sendmsg() return EACCES, which is the same error as it is returned if the user does not have the write bit in the traditional Unix file system permissions of that file.

This feature was created with substantial discussion and input from Justin Suess, Tingmao Wang and Mickaël Salaün.

---
 include/uapi/linux/landlock.h                |  10 ++
 security/landlock/access.h                   |  11 +-
 security/landlock/audit.c                    |   1 +
 security/landlock/fs.c                       | 102 ++++++++++++++++++-
 security/landlock/limits.h                   |   2 +-
 security/landlock/syscalls.c                 |   2 +-
 tools/testing/selftests/landlock/base_test.c |   2 +-
 tools/testing/selftests/landlock/fs_test.c   |   5 +-
 8 files changed, 128 insertions(+), 7 deletions(-)

index 60ff217ab95b..8d0edf94037d 100644

security/landlock/audit.c +1 -0
@@ -37,6 +37,7 @@ static const char *const fs_access_strings[] = {
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev",
+	[BIT_INDEX(LANDLOCK_ACCESS_FS_RESOLVE_UNIX)] = "fs.resolve_unix",
 };
 
 static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);
security/landlock/fs.c +3 -1
index e764470f588c..76035c6f2bf1 100644
@@ -27,6 +27,7 @@
 #include <linux/lsm_hooks.h>
 #include <linux/mount.h>
 #include <linux/namei.h>
+#include <linux/net.h>
 #include <linux/path.h>
 #include <linux/pid.h>
 #include <linux/rcupdate.h>
@@ -314,7 +315,8 @@ static struct landlock_object *get_inode_object(struct inode *const inode)
 	LANDLOCK_ACCESS_FS_WRITE_FILE | \
 	LANDLOCK_ACCESS_FS_READ_FILE | \
 	LANDLOCK_ACCESS_FS_TRUNCATE | \
-	LANDLOCK_ACCESS_FS_IOCTL_DEV)
+	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
 /* clang-format on */
 
 /*
@@ -1561,6 +1563,103 @@ static int hook_path_truncate(const struct path *const path)
 	return current_check_access_path(path, LANDLOCK_ACCESS_FS_TRUNCATE);
 }
 
Quoted 8 lines
Quoted 6 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1571

Quoted 6 lines
security/landlock/fs.c +6 -0
+/**
+ * unmask_scoped_access - Remove access right bits in @masks in all layers
+ *                        where @client and @server have the same domain
+ *
+ * This does the same as domain_is_scoped(), but unmasks bits in @masks.
+ * It can not return early as domain_is_scoped() does.

Why can't we use the same logic as for other scopes?

The other scopes, for which this is implemented in domain_is_scoped(), do not need to do this layer-by-layer.

I have to admit, in my initial implementation, I was using domain_is_scoped() directly, and the logic at the end of the hook was roughly:

   --- BUGGY CODE START ---
       // ...
       
       if (!domain_is_scoped(..., ..., LANDLOCK_ACCESS_FS_RESOLVE_UNIX))
           return 0;  /* permitted */

       return current_check_access_path(path, LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
   }
   --- BUGGY CODE END ---

Unfortunately, that is a logic error though -- it implements the formula

Quoted 7 lines

Access granted if:
(FOR-ALL l ∈ layers scoped-access-ok(l)) OR (FOR-ALL l ∈ layers path-access-ok(l)) (WRONG!)

but the formula we want is:

Access granted if:
FOR-ALL l ∈ layers (scoped-access-ok(l) OR path-access-ok(l)) (CORRECT!)

It is worth it to add this explanation to the unmask_scoped_access() description, also pointing to the test that check this case.

Quoted 70 lines

This makes a difference in the case where (pseudocode):

1. landlock_restrict_self(RESOLVE_UNIX) // d1
2. create_unix_server("./sock")
3. landlock_restrict_self(RESOLVE_UNIX, rule=Allow(".", RESOLVE_UNIX)) // d2
4. connect_unix("./sock")

,------------------------------------------------d1--,

   |                                                    |
   |    ./sock server                                   |
   |       ^                                            |
   |       |                                            |
   |  ,------------------------------------------d2--,  |
   |  |    |                                         |  |
   |  |  client                                      |  |
   |  |                                              |  |
   |  '----------------------------------------------'  |
   |                                                    |

'----------------------------------------------------'

(BTW, this scenario is covered in the selftests, that is why there is a variant of these selftests where instead of applying "no domain", we apply a domain with an exception rule like in step 3 in the pseudocode above. Applying that domain should behave the same as applying no domain at all.)

Intuitively, it is clear that the access should be granted:

- d1 does not restrict access to the server, because the socket was created within d1 itself.
- d2 does not restrict access to the server, because it has a rule to allow it

But the "buggy code" logic above comes to a different conclusion:

- the domain_is_scoped() check denies the access, because the server is in a more privileged domain relative to the client domain.
- the current_check_access_path() check denies the access as well, because the socket's path is not allow-listed in d1.

In the 'intuitive' reasoning above, we are checking d1 and d2 independently of each other. While Landlock is not implemented like that internally, we need to stay consistent with it so that domains compose correctly. The way to do that is to track is access check results on a per-layer basis again, and that is why unmask_scoped_access() uses a layer mask for tracking. The original domain_is_scoped() does not use a layer mask, but that also means that it can return early in some scenarios -- if for any of the relevant layer depths, the client and server domains are not the same, it exits early with failure because it's overall not fulfillable any more. In the RESOLVE_UNIX case though, we need to remember in which layers we failed (both high an low ones), because these layers can still be fulfilled with a PATH_BENEATH rule later.

Summary:

Option 1: We *can* unify this if you want. It just might come at a small performance penalty for domain_is_scoped(), which now uses the larger layer mask data structure and can't do the same early returns any more as before.

Option 2: Alternatively, if we move the two functions into the same module, we can keep them separate but still test them against each other to make sure they are in-line:

This invocation should return true...

domain_is_scoped(cli, srv, access)

Quoted 7 lines

...in the exactly the same situations where this invocation leaves any bits set in layer_masks:

  landlock_init_layer_masks(dom, access, &layer_masks, LL_KEY_INODE);
  unmask_scoped_access(cli, srv, &layer_masks, access);

What do you prefer?

I was thinking about factoring out domain_is_scoped() with unmask_scoped_access() but, after some tests, it is not worth it. Your approach is simple and good.

Quoted 20 lines
Quoted 20 lines
Quoted 15 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1581

Quoted 12 lines
security/landlock/fs.c +10 -0
+ *
+ * @client: Client domain
+ * @server: Server domain
+ * @masks: Layer access masks to unmask
+ * @access: Access bit that controls scoping
+ */
+static void unmask_scoped_access(const struct landlock_ruleset *const client,
+				 const struct landlock_ruleset *const server,
+				 struct layer_access_masks *const masks,
+				 const access_mask_t access)

This helper should be moved to task.c and factored out with domain_is_scoped(). This should be a dedicated patch.

Well, if domain_is_scoped() can be refactored and made generic, it would make more sense to move it to domain.c

Quoted 1 line
Quoted 6 lines
Quoted 6 lines
Quoted 6 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1587

Quoted 6 lines
security/landlock/fs.c +6 -0
+{
+	int client_layer, server_layer;
+	const struct landlock_hierarchy *client_walker, *server_walker;
+
+	if (WARN_ON_ONCE(!client))
+		return; /* should not happen */

Please no comment after ";"

Quoted 22 lines
Quoted 22 lines
Quoted 22 lines
Quoted 22 lines
security/landlock/fs.c +22 -0
+
+	if (!server)
+		return; /* server has no Landlock domain; nothing to clear */
+
+	client_layer = client->num_layers - 1;
+	client_walker = client->hierarchy;
+	server_layer = server->num_layers - 1;
+	server_walker = server->hierarchy;
+
+	/*
+	 * Clears the access bits at all layers where the client domain is the
+	 * same as the server domain.  We start the walk at min(client_layer,
+	 * server_layer).  The layer bits until there can not be cleared because
+	 * either the client or the server domain is missing.
+	 */
+	for (; client_layer > server_layer; client_layer--)
+		client_walker = client_walker->parent;
+
+	for (; server_layer > client_layer; server_layer--)
+		server_walker = server_walker->parent;
+
+	for (; client_layer >= 0; client_layer--) {
Quoted 8 lines
Quoted 8 lines
Quoted 2 lines
Quoted 2 lines
Quoted 2 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1611

Quoted 2 lines
security/landlock/fs.c +2 -0
+		if (masks->access[client_layer] & access &&
+		    client_walker == server_walker)

I'd prefer to first check client_walker == server_walker and then the access. My main concern is that only one bit of access matching masks->access[client_layer] clear all the access request bits. In practice there is only one, for now, but this code should be more strict by following a defensive approach.

This function works even if multiple access request bits with "scope-like" semantics were being checked in parallel; if you consider the logic in:

Quoted 6 lines
  if (masks->access[client_layer] & access &&
      client_walker == server_walker)
          masks->access[client_layer] &= ~access;

you'll realize that the check for "masks->access[client_layer] & access" is technically irrelevant - if that check fails, all the

Correct

Quoted 81 lines

affected bits are already zero, so clearing them is a no-op. This code is equivalent, but might perform slightly more writes (although it likely does not make a performance difference in practice):

  if (client_walker == server_walker)
          masks->access[client_layer] &= ~access;

With that code it's a bit easier to see that "access" is actually only used to decide which bits to clear. This works both with one and with multiple access rights.

This follows the same logic as outlined in the comment above in the code, where it says:

Clears the access bits at all layers where the client domain is the same as the server domain. We start the walk at min(client_layer, server_layer). The layer bits until there can not be cleared because either the client or the server domain is missing.

Clearing bits that aren't there is a no-op

<Optional Math>

I found it helpful to visualize the scoping logic, this is directly from my notes: (Web version is at https://wiki.gnoack.org/LandlockDomainIsScoped)

The domain_is_scoped() helper implements the following predicate:

∀ l ∈ (0,16): (hasbit(self, l) implies-that domain(self, l) == domain(other, l))

That is, we require for each layer l nesting depth that:

* **If** scoping is active at the layer,
* **Then** the domains of self and other are the same at the given nesting depth.

For example:

       [ ]
        |
       [x]     self and other have the same domain at this depth
        |
       [ ]
      /   \
    [x]   [ ]  self and other have differing domains at this depth
     |     |
    [ ]   [ ]
     |
    [ ]     "other"             "x" marks a domain where "self" has
                                    set the scoping bit

"self"

</Optional Math>

Quoted 4 lines
Quoted 1 line
Quoted 1 line
Quoted 1 line
Quoted 1 line

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1612

Quoted 1 line
security/landlock/fs.c +1 -0
+			masks->access[client_layer] &= ~access;

Actually, why not removing the access argument and just reset masks->access[client_layer]? The doc would need some updates.

It would feel brittle to me if this function were to clear out unrelated access rights. It receives a struct layer_access_masks after all, where it is normally expected that multiple kinds of access rights are set. In my understanding, the bit masking does not cost much extra performance compared to clearing it out entirely, so I'd prefer to have clearer semantics and only operate on the access rights that it's about, even when the other bits are all zero at the moment.

(For full disclosure, I have contemplated for a bit whether hook_unix_find() should take a layer_mask_t-like type where each bit indicates whether a given access right (LANDLOCK_ACCESS_FS_RESOLVE_UNIX, in this case) is set at a given layer, and then it would only clear out the bits there. That would be in some ways simpler, but then the caller would still need to convert back and forth to a layer mask anyway, because that's what the other functions there take. So it didn't seem like a good option in the bigger scheme (and I would also prefer to not re-introduce layer_mask_t after we just removed it).)

Quoted 2 lines

Maybe I did not understand your remark fully though; Does my argument sound reasonable?

Yes! Thanks for the deep explanation.

#21 lore raw

Re: [PATCH v5 2/9] landlock: Control pathname UNIX domain socket resolution by path

In reply to Mickaël Salaün

+netdev, we could use some advice on the locking approach in af_unix (see below)

Quoted 21 lines · Mickaël Salaün
Quoted 7 lines · Günther Noack
include/uapi/linux/landlock.h +0 -0
index f88fa1f68b77..3a8fc3af0d64 100644
@@ -248,6 +248,15 @@ struct landlock_net_port_attr {
  *
  *   This access right is available since the fifth version of the Landlock

include/uapi/linux/landlock.h struct landlock_net_port_attr { · line 259

Quoted 10 lines
include/uapi/linux/landlock.h +9 -0
  *   ABI.
+ * - %LANDLOCK_ACCESS_FS_RESOLVE_UNIX: Look up pathname UNIX domain sockets
+ *   (:manpage:`unix(7)`).  On UNIX domain sockets, this restricts both calls to
+ *   :manpage:`connect(2)` as well as calls to :manpage:`sendmsg(2)` with an
+ *   explicit recipient address.
+ *
+ *   This access right only applies to connections to UNIX server sockets which
+ *   were created outside of the newly created Landlock domain (e.g. from within
+ *   a parent domain or from an unrestricted process).  Newly created UNIX
+ *   servers within the same Landlock domain continue to be accessible.

It might help to add a reference to the explicit scope mechanism.

Quoted 2 lines

Please squash patch 9/9 into this one and also add a reference here to the rationale described in security/landlock.rst

Sounds good, will do.

Quoted 7 lines

include/uapi/linux/landlock.h struct landlock_net_port_attr {

Quoted 4 lines
include/uapi/linux/landlock.h +4 -0
+static void unmask_scoped_access(const struct landlock_ruleset *const client,
+				 const struct landlock_ruleset *const server,
+				 struct layer_access_masks *const masks,
+				 const access_mask_t access)

This helper should be moved to task.c and factored out with domain_is_scoped(). This should be a dedicated patch.

(already discussed in another follow-up mail)

Quoted 41 lines
Quoted 18 lines
include/uapi/linux/landlock.h +18 -0
+static int hook_unix_find(const struct path *const path, struct sock *other,
+			  int flags)
+{
+	const struct landlock_ruleset *dom_other;
+	const struct landlock_cred_security *subject;
+	struct layer_access_masks layer_masks;
+	struct landlock_request request = {};
+	static const struct access_masks fs_resolve_unix = {
+		.fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+	};
+
+	/* Lookup for the purpose of saving coredumps is OK. */
+	if (unlikely(flags & SOCK_COREDUMP))
+		return 0;
+
+	/* Access to the same (or a lower) domain is always allowed. */
+	subject = landlock_get_applicable_subject(current_cred(),
+						  fs_resolve_unix, NULL);

include/uapi/linux/landlock.h

Quoted 10 lines
include/uapi/linux/landlock.h +10 -0
+
+	if (!subject)
+		return 0;
+
+	if (!landlock_init_layer_masks(subject->domain, fs_resolve_unix.fs,
+				       &layer_masks, LANDLOCK_KEY_INODE))
+		return 0;
+
+	/* Checks the layers in which we are connecting within the same domain. */
+	dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain;

We need to call unix_state_lock(other) before reading it, and check for SOCK_DEAD, and check sk_socket before dereferencing it. Indeed, the socket can be make orphan (see unix_dgram_sendmsg and unix_stream_connect). I *think* a socket cannot be "resurrected" or recycled once dead, so we may assume there is no race condition wrt dom_other, but please double check. This lockless call should be made clear in the LSM hook. It's OK to not lock the socket before security_unix_find() (1) because no LSM might implement and (2) they might not need to lock the socket (e.g. if the caller is not sandboxed).

The updated code should look something like this:

Quoted 8 lines

unix_state_unlock(other);

if (unlikely(sock_flag(other, SOCK_DEAD) || !other->sk_socket)) {
	unix_state_unlock(other);
	return 0;
}

dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain; unix_state_unlock(other);

Thank you for spotting the locking concern!

@anyone from netdev, could you please advise on the correct locking approach here?

* Do we need ot check SOCK_DEAD?

You are saying that we need to do that, but it's not clear to me why.

If you look at the places where unix_find_other() is called in af_unix.c, then you'll find that all of them check for SOCK_DEAD and then restart from unix_find_other() if they do actually discover that the socket is dead. I think that this is catching this race condition scenario:

* a server socket exists and is alive
* A client connects: af_unix.c's unix_stream_connect() calls unix_find_other() and finds the server socket...
* (Concurrently): The server closes the socket and enters unix_release_sock(). This function:
1. disassociates the server sock from the named socket inode number in the hash table (=> future unix_find_other() calls will fail).
2. calls sock_orphan(), which sets SOCK_DEAD.
* ...(client connection resuming): unix_stream_connect() continues, grabs the unix_state_lock(), which apparently protects everything here, checks that the socket is not dead - and discovers that it IS suddenly dead. This was not supposed to happen. The code recovers from that by retrying everything starting with the unix_find_other() call. From unix_release_sock(), we know that the inode is not associated with the sock any more -- so the unix_find_socket_by_inode() call should be failing on the next attempt.

(This works with unix_dgram_connect() and unix_dgram_sendmsg() as well.)

The comments next to the SOCK_DEAD checks are also suggesting this.

* What lock to use

I am having trouble reasoning about what lock is used for what in this code.

Is it possible that the lock protecting ->sk_socket is the ->sk_callback_lock, not the unix_state_lock()? The only callers to sk_set_socket are either sock_orphan/sock_graft (both grabbing sk_callback_lock), or they are creating new struct sock objects that they own exclusively, and don't need locks yet.

Admittedly, in af_unix.c, sock_orphan() and sock_graft() only get called in contexts where the unix_state_lock() is held, so it would probably work as well to lock that, but it is maybe a more fine-grained approach to use sk_callback_lock?

So... how about a scheme where we only check for ->sk_socket not being NULL:

read_lock_bh(&other->sk_callback_lock);
struct sock *other_sk = other->sk_socket;

if (!other_sk) {
	read_unlock_bh(&other->sk_callback_lock);
	return 0;
}
/* XXX double check whether we need a lock here too */
struct file *file = other_sk->file;
if (!other_file) {
	read_unlock_bh(&other->sk_callback_lock);
	return 0;
}

read_unlock_bh(&other->sk_callback_lock);

If this fails, that would in my understanding also be because the socket has died after the path lookup. We'd then return 0 (success), because we know that the surrounding SOCK_DEAD logic will repeat everything starting from the path lookup operation (this time likely failing with ECONNREFUSED, but maybe also with a success, if another server process was quick enough).

Does this sound reasonable?
–Günther

maintainer
#22 lore raw

Re: [PATCH v5 2/9] landlock: Control pathname UNIX domain socket resolution by path

Quoted 38 lines · Günther Noack

+netdev, we could use some advice on the locking approach in af_unix (see below)

Quoted 21 lines · Mickaël Salaün
Quoted 7 lines · Günther Noack
include/uapi/linux/landlock.h +0 -0
index f88fa1f68b77..3a8fc3af0d64 100644
@@ -248,6 +248,15 @@ struct landlock_net_port_attr {
  *
  *   This access right is available since the fifth version of the Landlock

include/uapi/linux/landlock.h struct landlock_net_port_attr { · line 259

Quoted 10 lines
include/uapi/linux/landlock.h +9 -0
  *   ABI.
+ * - %LANDLOCK_ACCESS_FS_RESOLVE_UNIX: Look up pathname UNIX domain sockets
+ *   (:manpage:`unix(7)`).  On UNIX domain sockets, this restricts both calls to
+ *   :manpage:`connect(2)` as well as calls to :manpage:`sendmsg(2)` with an
+ *   explicit recipient address.
+ *
+ *   This access right only applies to connections to UNIX server sockets which
+ *   were created outside of the newly created Landlock domain (e.g. from within
+ *   a parent domain or from an unrestricted process).  Newly created UNIX
+ *   servers within the same Landlock domain continue to be accessible.

It might help to add a reference to the explicit scope mechanism.

Quoted 2 lines

Please squash patch 9/9 into this one and also add a reference here to the rationale described in security/landlock.rst

Sounds good, will do.

Quoted 7 lines

include/uapi/linux/landlock.h struct landlock_net_port_attr {

Quoted 4 lines
include/uapi/linux/landlock.h +4 -0
+static void unmask_scoped_access(const struct landlock_ruleset *const client,
+				 const struct landlock_ruleset *const server,
+				 struct layer_access_masks *const masks,
+				 const access_mask_t access)

This helper should be moved to task.c and factored out with domain_is_scoped(). This should be a dedicated patch.

Quoted 45 lines

(already discussed in another follow-up mail)

Quoted 42 lines
Quoted 18 lines
include/uapi/linux/landlock.h +18 -0
+static int hook_unix_find(const struct path *const path, struct sock *other,
+			  int flags)
+{
+	const struct landlock_ruleset *dom_other;
+	const struct landlock_cred_security *subject;
+	struct layer_access_masks layer_masks;
+	struct landlock_request request = {};
+	static const struct access_masks fs_resolve_unix = {
+		.fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+	};
+
+	/* Lookup for the purpose of saving coredumps is OK. */
+	if (unlikely(flags & SOCK_COREDUMP))
+		return 0;
+
+	/* Access to the same (or a lower) domain is always allowed. */
+	subject = landlock_get_applicable_subject(current_cred(),
+						  fs_resolve_unix, NULL);

include/uapi/linux/landlock.h

Quoted 10 lines
include/uapi/linux/landlock.h +10 -0
+
+	if (!subject)
+		return 0;
+
+	if (!landlock_init_layer_masks(subject->domain, fs_resolve_unix.fs,
+				       &layer_masks, LANDLOCK_KEY_INODE))
+		return 0;
+
+	/* Checks the layers in which we are connecting within the same domain. */
+	dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain;

We need to call unix_state_lock(other) before reading it, and check for SOCK_DEAD, and check sk_socket before dereferencing it. Indeed, the socket can be make orphan (see unix_dgram_sendmsg and unix_stream_connect). I *think* a socket cannot be "resurrected" or recycled once dead, so we may assume there is no race condition wrt dom_other, but please double check. This lockless call should be made clear in the LSM hook. It's OK to not lock the socket before security_unix_find() (1) because no LSM might implement and (2) they might not need to lock the socket (e.g. if the caller is not sandboxed).

The updated code should look something like this:

unix_state_unlock(other);

unix_state_lock(other) of course...

Quoted 46 lines
Quoted 7 lines
if (unlikely(sock_flag(other, SOCK_DEAD) || !other->sk_socket)) {
	unix_state_unlock(other);
	return 0;
}

dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain; unix_state_unlock(other);

Thank you for spotting the locking concern!

@anyone from netdev, could you please advise on the correct locking approach here?

* Do we need ot check SOCK_DEAD?

You are saying that we need to do that, but it's not clear to me why.

If you look at the places where unix_find_other() is called in af_unix.c, then you'll find that all of them check for SOCK_DEAD and then restart from unix_find_other() if they do actually discover that the socket is dead. I think that this is catching this race condition scenario:

* a server socket exists and is alive
* A client connects: af_unix.c's unix_stream_connect() calls unix_find_other() and finds the server socket...
* (Concurrently): The server closes the socket and enters unix_release_sock(). This function:
1. disassociates the server sock from the named socket inode number in the hash table (=> future unix_find_other() calls will fail).
2. calls sock_orphan(), which sets SOCK_DEAD.
* ...(client connection resuming): unix_stream_connect() continues, grabs the unix_state_lock(), which apparently protects everything here, checks that the socket is not dead - and discovers that it IS suddenly dead. This was not supposed to happen. The code recovers from that by retrying everything starting with the unix_find_other() call. From unix_release_sock(), we know that the inode is not associated with the sock any more -- so the unix_find_socket_by_inode() call should be failing on the next attempt.

(This works with unix_dgram_connect() and unix_dgram_sendmsg() as well.)

Quoted 6 lines

The comments next to the SOCK_DEAD checks are also suggesting this.

* What lock to use

I am having trouble reasoning about what lock is used for what in this code.

It's not clear to me neither, and it looks like it's not consistent across protocols.

Quoted 30 lines

Is it possible that the lock protecting ->sk_socket is the ->sk_callback_lock, not the unix_state_lock()? The only callers to sk_set_socket are either sock_orphan/sock_graft (both grabbing sk_callback_lock), or they are creating new struct sock objects that they own exclusively, and don't need locks yet.

Admittedly, in af_unix.c, sock_orphan() and sock_graft() only get called in contexts where the unix_state_lock() is held, so it would probably work as well to lock that, but it is maybe a more fine-grained approach to use sk_callback_lock?

So... how about a scheme where we only check for ->sk_socket not being NULL:

read_lock_bh(&other->sk_callback_lock);
struct sock *other_sk = other->sk_socket;

if (!other_sk) {
	read_unlock_bh(&other->sk_callback_lock);
	return 0;
}
/* XXX double check whether we need a lock here too */
struct file *file = other_sk->file;
if (!other_file) {
	read_unlock_bh(&other->sk_callback_lock);
	return 0;
}

read_unlock_bh(&other->sk_callback_lock);

Quoted 8 lines

If this fails, that would in my understanding also be because the socket has died after the path lookup. We'd then return 0 (success), because we know that the surrounding SOCK_DEAD logic will repeat everything starting from the path lookup operation (this time likely failing with ECONNREFUSED, but maybe also with a success, if another server process was quick enough).

Does this sound reasonable?

Actually, since commit 983512f3a87f ("net: Drop the lock in skb_may_tx_timestamp()"), we can just use RCU + READ_ONCE(sk_socket) + READ_ONCE(file). The socket and file should only be freed after the RCU grace periode. As a safeguard, this commit should be a Depends-on.

However, it is safer to return -ECONNREFULED when sk_socket or file are NULL.

I would be good to hear from netdev folks though.

TIL, there is an LSM hook for sock_graft().

Quoted 2 lines

–Günther

#23 lore raw

Re: [PATCH v5 2/9] landlock: Control pathname UNIX domain socket resolution by path

Quoted 5 lines · Mickaël Salaün
Quoted 5 lines
Quoted 5 lines

dom_other, but please double check. This lockless call should be made clear in the LSM hook. It's OK to not lock the socket before security_unix_find() (1) because no LSM might implement and (2) they might not need to lock the socket (e.g. if the caller is not sandboxed).

Quoted 18 lines
Quoted 3 lines
Quoted 3 lines

The updated code should look something like this:

unix_state_unlock(other);

unix_state_lock(other) of course...

Quoted 12 lines
Quoted 7 lines
if (unlikely(sock_flag(other, SOCK_DEAD) || !other->sk_socket)) {
	unix_state_unlock(other);
	return 0;
}

dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain; unix_state_unlock(other);

Thank you for spotting the locking concern!

@anyone from netdev, could you please advise on the correct locking approach here?

It is hard to tell where your "other" is from. So it is not clear to me if the sock can be closed from the other side. If it can then sk_socket becomes NULL and everything afterwards will be gone.

Therefore checking for SOCK_DEAD under unix_state_lock() looks sane.

Quoted 30 lines
Quoted 30 lines

* Do we need ot check SOCK_DEAD?

You are saying that we need to do that, but it's not clear to me why.

If you look at the places where unix_find_other() is called in af_unix.c, then you'll find that all of them check for SOCK_DEAD and then restart from unix_find_other() if they do actually discover that the socket is dead. I think that this is catching this race condition scenario:

* a server socket exists and is alive
* A client connects: af_unix.c's unix_stream_connect() calls unix_find_other() and finds the server socket...
* (Concurrently): The server closes the socket and enters unix_release_sock(). This function:
1. disassociates the server sock from the named socket inode number in the hash table (=> future unix_find_other() calls will fail).
2. calls sock_orphan(), which sets SOCK_DEAD.
* ...(client connection resuming): unix_stream_connect() continues, grabs the unix_state_lock(), which apparently protects everything here, checks that the socket is not dead - and discovers that it IS suddenly dead. This was not supposed to happen. The code recovers from that by retrying everything starting with the unix_find_other() call. From unix_release_sock(), we know that the inode is not associated with the sock any more -- so the unix_find_socket_by_inode() call should be failing on the next attempt.

Quoted 4 lines
Quoted 4 lines

(This works with unix_dgram_connect() and unix_dgram_sendmsg() as well.)

The comments next to the SOCK_DEAD checks are also suggesting this.

Sure. You are not the owner I guess. So you hold a reference on it but the owner can still close it.

Quoted 20 lines
Quoted 5 lines

* What lock to use

I am having trouble reasoning about what lock is used for what in this code.

It's not clear to me neither, and it looks like it's not consistent across protocols.

Quoted 11 lines

Is it possible that the lock protecting ->sk_socket is the ->sk_callback_lock, not the unix_state_lock()? The only callers to sk_set_socket are either sock_orphan/sock_graft (both grabbing sk_callback_lock), or they are creating new struct sock objects that they own exclusively, and don't need locks yet.

Admittedly, in af_unix.c, sock_orphan() and sock_graft() only get called in contexts where the unix_state_lock() is held, so it would probably work as well to lock that, but it is maybe a more fine-grained approach to use sk_callback_lock?

This is correct. Since only sock_orphan() is used you could go for sk_callback_lock. For simplicity you could stick to the former lock which will be accessed later any way. Either of the two block setting of DEAD.

Quoted 17 lines
Quoted 17 lines

So... how about a scheme where we only check for ->sk_socket not being NULL:

read_lock_bh(&other->sk_callback_lock);
struct sock *other_sk = other->sk_socket;

if (!other_sk) {
	read_unlock_bh(&other->sk_callback_lock);
	return 0;
}
/* XXX double check whether we need a lock here too */
struct file *file = other_sk->file;
if (!other_file) {
	read_unlock_bh(&other->sk_callback_lock);
	return 0;
}

read_unlock_bh(&other->sk_callback_lock);

Quoted 8 lines
Quoted 8 lines

If this fails, that would in my understanding also be because the socket has died after the path lookup. We'd then return 0 (success), because we know that the surrounding SOCK_DEAD logic will repeat everything starting from the path lookup operation (this time likely failing with ECONNREFUSED, but maybe also with a success, if another server process was quick enough).

Does this sound reasonable?

So if SOCK_DEAD is not set while the lock is held you can reference the chain without second thoughts.

Quoted 4 lines

Actually, since commit 983512f3a87f ("net: Drop the lock in skb_may_tx_timestamp()"), we can just use RCU + READ_ONCE(sk_socket) + READ_ONCE(file). The socket and file should only be freed after the RCU grace periode. As a safeguard, this commit should be a Depends-on.

This is what I concluded. The commit in question did not change the situation. But if this spreads more I would suggest a helper so that all user of this short cut can be easily identified. And yes, RCU would be a key requirement.

Quoted 8 lines

However, it is safer to return -ECONNREFULED when sk_socket or file are NULL.

I would be good to hear from netdev folks though.

TIL, there is an LSM hook for sock_graft().

Quoted 1 line

–Günther

Sebastian

#24 lore raw

Re: [PATCH v5 2/9] landlock: Control pathname UNIX domain socket resolution by path

In reply to Mickaël Salaün
Quoted 82 lines · Mickaël Salaün
Quoted 80 lines · Günther Noack

+netdev, we could use some advice on the locking approach in af_unix (see below)

Quoted 21 lines · Mickaël Salaün
Quoted 7 lines · Günther Noack
include/uapi/linux/landlock.h +0 -0
index f88fa1f68b77..3a8fc3af0d64 100644
@@ -248,6 +248,15 @@ struct landlock_net_port_attr {
  *
  *   This access right is available since the fifth version of the Landlock

include/uapi/linux/landlock.h struct landlock_net_port_attr { · line 259

Quoted 10 lines
include/uapi/linux/landlock.h +9 -0
  *   ABI.
+ * - %LANDLOCK_ACCESS_FS_RESOLVE_UNIX: Look up pathname UNIX domain sockets
+ *   (:manpage:`unix(7)`).  On UNIX domain sockets, this restricts both calls to
+ *   :manpage:`connect(2)` as well as calls to :manpage:`sendmsg(2)` with an
+ *   explicit recipient address.
+ *
+ *   This access right only applies to connections to UNIX server sockets which
+ *   were created outside of the newly created Landlock domain (e.g. from within
+ *   a parent domain or from an unrestricted process).  Newly created UNIX
+ *   servers within the same Landlock domain continue to be accessible.

It might help to add a reference to the explicit scope mechanism.

Quoted 2 lines

Please squash patch 9/9 into this one and also add a reference here to the rationale described in security/landlock.rst

Sounds good, will do.

Quoted 7 lines

include/uapi/linux/landlock.h struct landlock_net_port_attr {

Quoted 4 lines
include/uapi/linux/landlock.h +4 -0
+static void unmask_scoped_access(const struct landlock_ruleset *const client,
+                          const struct landlock_ruleset *const server,
+                          struct layer_access_masks *const masks,
+                          const access_mask_t access)

This helper should be moved to task.c and factored out with domain_is_scoped(). This should be a dedicated patch.

(already discussed in another follow-up mail)

Quoted 39 lines
Quoted 18 lines
include/uapi/linux/landlock.h +18 -0
+static int hook_unix_find(const struct path *const path, struct sock *other,
+                   int flags)
+{
+ const struct landlock_ruleset *dom_other;
+ const struct landlock_cred_security *subject;
+ struct layer_access_masks layer_masks;
+ struct landlock_request request = {};
+ static const struct access_masks fs_resolve_unix = {
+         .fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+ };
+
+ /* Lookup for the purpose of saving coredumps is OK. */
+ if (unlikely(flags & SOCK_COREDUMP))
+         return 0;
+
+ /* Access to the same (or a lower) domain is always allowed. */
+ subject = landlock_get_applicable_subject(current_cred(),
+                                           fs_resolve_unix, NULL);

include/uapi/linux/landlock.h

Quoted 10 lines
include/uapi/linux/landlock.h +10 -0
+
+ if (!subject)
+         return 0;
+
+ if (!landlock_init_layer_masks(subject->domain, fs_resolve_unix.fs,
+                                &layer_masks, LANDLOCK_KEY_INODE))
+         return 0;
+
+ /* Checks the layers in which we are connecting within the same domain. */
+ dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain;

We need to call unix_state_lock(other) before reading it, and check for SOCK_DEAD, and check sk_socket before dereferencing it. Indeed, the socket can be make orphan (see unix_dgram_sendmsg and unix_stream_connect). I *think* a socket cannot be "resurrected" or recycled once dead, so we may assume there is no race condition wrt dom_other, but please double check. This lockless call should be made clear in the LSM hook. It's OK to not lock the socket before security_unix_find() (1) because no LSM might implement and (2) they might not need to lock the socket (e.g. if the caller is not sandboxed).

Quoted 20 lines
Quoted 3 lines
Quoted 3 lines

The updated code should look something like this:

unix_state_unlock(other);

unix_state_lock(other) of course...

Quoted 14 lines
Quoted 7 lines
if (unlikely(sock_flag(other, SOCK_DEAD) || !other->sk_socket)) {
    unix_state_unlock(other);
    return 0;
}

dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain; unix_state_unlock(other);

Thank you for spotting the locking concern!

@anyone from netdev, could you please advise on the correct locking approach here?

* Do we need ot check SOCK_DEAD?

It depends ? But I don't see your full patch and have no idea how it will be used.

Quoted 8 lines
Quoted 8 lines

You are saying that we need to do that, but it's not clear to me why.

If you look at the places where unix_find_other() is called in af_unix.c, then you'll find that all of them check for SOCK_DEAD and then restart from unix_find_other() if they do actually discover that the socket is dead.

Note all callers of unix_find_other() later locks the other and double check SOCK_DEAD.

The same check in unix_find_other() is to avoid unnecessary locking in case the socket is dying.

Quoted 27 lines
Quoted 27 lines

I think that this is catching this race
condition scenario:

* a server socket exists and is alive
* A client connects: af_unix.c's unix_stream_connect() calls unix_find_other() and finds the server socket...
* (Concurrently): The server closes the socket and enters unix_release_sock(). This function:
1. disassociates the server sock from the named socket inode number in the hash table (=> future unix_find_other() calls will fail).
2. calls sock_orphan(), which sets SOCK_DEAD.
* ...(client connection resuming): unix_stream_connect() continues, grabs the unix_state_lock(), which apparently protects everything here, checks that the socket is not dead - and discovers that it IS suddenly dead. This was not supposed to happen. The code recovers from that by retrying everything starting with the unix_find_other() call. From unix_release_sock(), we know that the inode is not associated with the sock any more -- so the unix_find_socket_by_inode() call should be failing on the next attempt.

(This works with unix_dgram_connect() and unix_dgram_sendmsg() as well.)

The comments next to the SOCK_DEAD checks are also suggesting this.

Quoted 11 lines
Quoted 4 lines

* What lock to use

I am having trouble reasoning about what lock is used for what in this code.

It's not clear to me neither, and it looks like it's not consistent across protocols.

Quoted 3 lines

Is it possible that the lock protecting ->sk_socket is the ->sk_callback_lock, not the unix_state_lock()?

Yes, but unix_state_lock() is better.

BPF SOCKMAP is the major user of sk_callback_lock.

Quoted 35 lines
Quoted 35 lines

The only callers to
sk_set_socket are either sock_orphan/sock_graft (both grabbing sk_callback_lock), or they are creating new struct sock objects that they own exclusively, and don't need locks yet.

Admittedly, in af_unix.c, sock_orphan() and sock_graft() only get called in contexts where the unix_state_lock() is held, so it would probably work as well to lock that, but it is maybe a more fine-grained approach to use sk_callback_lock?

So... how about a scheme where we only check for ->sk_socket not being NULL:

read_lock_bh(&other->sk_callback_lock);
struct sock *other_sk = other->sk_socket;

if (!other_sk) {
      read_unlock_bh(&other->sk_callback_lock);
      return 0;
}
/* XXX double check whether we need a lock here too */
struct file *file = other_sk->file;
if (!other_file) {
      read_unlock_bh(&other->sk_callback_lock);
      return 0;
}

read_unlock_bh(&other->sk_callback_lock);

If this fails, that would in my understanding also be because the socket has died after the path lookup. We'd then return 0 (success), because we know that the surrounding SOCK_DEAD logic will repeat everything starting from the path lookup operation (this time likely failing with ECONNREFUSED, but maybe also with a success, if another server process was quick enough).

Quoted 6 lines
Quoted 1 line

Does this sound reasonable?

Actually, since commit 983512f3a87f ("net: Drop the lock in skb_may_tx_timestamp()"), we can just use RCU + READ_ONCE(sk_socket) + READ_ONCE(file). The socket and file should only be freed after the RCU grace periode. As a safeguard, this commit should be a Depends-on.

Note this commit is for the networking fast path (interrupt context), where we want to avoid unnecessary locking as much as possible.

AF_UNIX works in the process context only and does not need to follow the pattern.

Quoted 10 lines

However, it is safer to return -ECONNREFULED when sk_socket or file are NULL.

I would be good to hear from netdev folks though.

TIL, there is an LSM hook for sock_graft().

Quoted 2 lines

–Günther

maintainer
#25 lore raw

Re: [PATCH v5 2/9] landlock: Control pathname UNIX domain socket resolution by path

In reply to Günther Noack
Quoted 12 lines · Günther Noack

* Add a new access right LANDLOCK_ACCESS_FS_RESOLVE_UNIX, which controls the look up operations for named UNIX domain sockets. The resolution happens during connect() and sendmsg() (depending on socket type).
* Hook into the path lookup in unix_find_bsd() in af_unix.c, using a LSM hook. Make policy decisions based on the new access rights
* Increment the Landlock ABI version.
* Minor test adaptions to keep the tests working.

With this access right, access is granted if either of the following conditions is met:

Quoted 8 lines

* The target socket's filesystem path was allow-listed using a LANDLOCK_RULE_PATH_BENEATH rule, *or*:
* The target socket was created in the same Landlock domain in which LANDLOCK_ACCESS_FS_RESOLVE_UNIX was restricted.

In case of a denial, connect() and sendmsg() return EACCES, which is the same error as it is returned if the user does not have the write bit in the traditional Unix file system permissions of that file.

It is not the same error code as for scoped abstract unix socket (EPERM), but it makes sense because the scope restrictions are closer to ambient rights (i.e. similar to a network isolation), whereas here the final denial comes from a missing FS rule (and all FS access checks may return EACCES). It would be worth mentioning this difference in the user documentation.

Quoted 43 lines

This feature was created with substantial discussion and input from Justin Suess, Tingmao Wang and Mickaël Salaün.

---
 include/uapi/linux/landlock.h                |  10 ++
 security/landlock/access.h                   |  11 +-
 security/landlock/audit.c                    |   1 +
 security/landlock/fs.c                       | 102 ++++++++++++++++++-
 security/landlock/limits.h                   |   2 +-
 security/landlock/syscalls.c                 |   2 +-
 tools/testing/selftests/landlock/base_test.c |   2 +-
 tools/testing/selftests/landlock/fs_test.c   |   5 +-
 8 files changed, 128 insertions(+), 7 deletions(-)
security/landlock/fs.c +22 -0
+static int hook_unix_find(const struct path *const path, struct sock *other,
+			  int flags)
+{
+	const struct landlock_ruleset *dom_other;
+	const struct landlock_cred_security *subject;
+	struct layer_access_masks layer_masks;
+	struct landlock_request request = {};
+	static const struct access_masks fs_resolve_unix = {
+		.fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+	};
+
+	/* Lookup for the purpose of saving coredumps is OK. */
+	if (unlikely(flags & SOCK_COREDUMP))
+		return 0;
+
+	/* Access to the same (or a lower) domain is always allowed. */
+	subject = landlock_get_applicable_subject(current_cred(),
+						  fs_resolve_unix, NULL);
+
+	if (!subject)
+		return 0;
+

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1650

Quoted 10 lines
security/landlock/fs.c +10 -0
+	if (!landlock_init_layer_masks(subject->domain, fs_resolve_unix.fs,
+				       &layer_masks, LANDLOCK_KEY_INODE))
+		return 0;
+
+	/* Checks the layers in which we are connecting within the same domain. */
+	dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain;
+	unmask_scoped_access(subject->domain, dom_other, &layer_masks,
+			     fs_resolve_unix.fs);
+
+	if (layer_access_masks_empty(&layer_masks))

I don't see the point of this helper and this call wrt the following is_access_to_paths_allowed() call and the is_layer_masks_allowed() check.

Quoted 11 lines
security/landlock/fs.c +11 -0
+		return 0;
+
+	/* Checks the connections to allow-listed paths. */
+	if (is_access_to_paths_allowed(subject->domain, path,
+				       fs_resolve_unix.fs, &layer_masks,
+				       &request, NULL, 0, NULL, NULL, NULL))
+		return 0;
+
+	landlock_log_denial(subject, &request);
+	return -EACCES;
+}
#26 lore raw

Re: [PATCH v5 2/9] landlock: Control pathname UNIX domain socket resolution by path

Quoted 22 lines · Mickaël Salaün
Quoted 12 lines · Günther Noack

* Add a new access right LANDLOCK_ACCESS_FS_RESOLVE_UNIX, which controls the look up operations for named UNIX domain sockets. The resolution happens during connect() and sendmsg() (depending on socket type).
* Hook into the path lookup in unix_find_bsd() in af_unix.c, using a LSM hook. Make policy decisions based on the new access rights
* Increment the Landlock ABI version.
* Minor test adaptions to keep the tests working.

With this access right, access is granted if either of the following conditions is met:

Quoted 8 lines

* The target socket's filesystem path was allow-listed using a LANDLOCK_RULE_PATH_BENEATH rule, *or*:
* The target socket was created in the same Landlock domain in which LANDLOCK_ACCESS_FS_RESOLVE_UNIX was restricted.

In case of a denial, connect() and sendmsg() return EACCES, which is the same error as it is returned if the user does not have the write bit in the traditional Unix file system permissions of that file.

Quoted 6 lines

It is not the same error code as for scoped abstract unix socket (EPERM), but it makes sense because the scope restrictions are closer to ambient rights (i.e. similar to a network isolation), whereas here the final denial comes from a missing FS rule (and all FS access checks may return EACCES). It would be worth mentioning this difference in the user documentation.

Sounds good, added to the syscall documentation for V6.

Quoted 48 lines
Quoted 48 lines

This feature was created with substantial discussion and input from Justin Suess, Tingmao Wang and Mickaël Salaün.

---
 include/uapi/linux/landlock.h                |  10 ++
 security/landlock/access.h                   |  11 +-
 security/landlock/audit.c                    |   1 +
 security/landlock/fs.c                       | 102 ++++++++++++++++++-
 security/landlock/limits.h                   |   2 +-
 security/landlock/syscalls.c                 |   2 +-
 tools/testing/selftests/landlock/base_test.c |   2 +-
 tools/testing/selftests/landlock/fs_test.c   |   5 +-
 8 files changed, 128 insertions(+), 7 deletions(-)
security/landlock/fs.c +28 -0
+static int hook_unix_find(const struct path *const path, struct sock *other,
+			  int flags)
+{
+	const struct landlock_ruleset *dom_other;
+	const struct landlock_cred_security *subject;
+	struct layer_access_masks layer_masks;
+	struct landlock_request request = {};
+	static const struct access_masks fs_resolve_unix = {
+		.fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+	};
+
+	/* Lookup for the purpose of saving coredumps is OK. */
+	if (unlikely(flags & SOCK_COREDUMP))
+		return 0;
+
+	/* Access to the same (or a lower) domain is always allowed. */
+	subject = landlock_get_applicable_subject(current_cred(),
+						  fs_resolve_unix, NULL);
+
+	if (!subject)
+		return 0;
+
+	if (!landlock_init_layer_masks(subject->domain, fs_resolve_unix.fs,
+				       &layer_masks, LANDLOCK_KEY_INODE))
+		return 0;
+
+	/* Checks the layers in which we are connecting within the same domain. */
+	dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain;
Quoted 8 lines

security/landlock/fs.c static int hook_path_truncate(const struct path *const path) · line 1650

Quoted 4 lines
security/landlock/fs.c +4 -0
+	unmask_scoped_access(subject->domain, dom_other, &layer_masks,
+			     fs_resolve_unix.fs);
+
+	if (layer_access_masks_empty(&layer_masks))

I don't see the point of this helper and this call wrt the following is_access_to_paths_allowed() call and the is_layer_masks_allowed() check.

layer_access_masks_empty() is indeed the same thing as is_layer_masks_allowed(), so I removed that implementation again for V6.

The reason why I was calling this here is so that we can skip the path walk in the case where the scoped-access check already suffices to allow the operation. It is not strictly needed though, so I can remove it. It is probably better to implement such a shortcut within is_access_to_paths_allowed() instead.

Removed the call and the implementation for V6.

Quoted 11 lines
Quoted 11 lines
security/landlock/fs.c +11 -0
+		return 0;
+
+	/* Checks the connections to allow-listed paths. */
+	if (is_access_to_paths_allowed(subject->domain, path,
+				       fs_resolve_unix.fs, &layer_masks,
+				       &request, NULL, 0, NULL, NULL, NULL))
+		return 0;
+
+	landlock_log_denial(subject, &request);
+	return -EACCES;
+}
#27 lore raw

PATCH v5 3/9 samples/landlock: Add support for named UNIX domain socket restrictions

In reply to Günther Noack

The access right for UNIX domain socket lookups is grouped with the read-write rights in the sample tool. Rationale: In the general case, any operations are possible through a UNIX domain socket, including data-mutating operations.

Cc Justin Suess <[email protected]>
Cc Mickaël Salaün <[email protected]>
---
 samples/landlock/sandboxer.c | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)
samples/landlock/sandboxer.c +12 -3
index e7af02f98208..0bbbc5c9ead6 100644
@@ -111,7 +111,8 @@ static int parse_path(char *env_path, const char ***const path_list)
 	LANDLOCK_ACCESS_FS_WRITE_FILE | \
 	LANDLOCK_ACCESS_FS_READ_FILE | \
 	LANDLOCK_ACCESS_FS_TRUNCATE | \
-	LANDLOCK_ACCESS_FS_IOCTL_DEV)
+	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
 
 /* clang-format on */
 
@@ -295,11 +296,12 @@ static bool check_ruleset_scope(const char *const env_var,
 	LANDLOCK_ACCESS_FS_MAKE_SYM | \
 	LANDLOCK_ACCESS_FS_REFER | \
 	LANDLOCK_ACCESS_FS_TRUNCATE | \
-	LANDLOCK_ACCESS_FS_IOCTL_DEV)
+	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
 
 /* clang-format on */
 
-#define LANDLOCK_ABI_LAST 7
+#define LANDLOCK_ABI_LAST 9
 
 #define XSTR(s) #s
 #define STR(s) XSTR(s)
@@ -444,6 +446,13 @@ int main(const int argc, char *const argv[], char *const *const envp)
 			"provided by ABI version %d (instead of %d).\n",
 			LANDLOCK_ABI_LAST, abi);
 		__attribute__((fallthrough));
+	case 7:
+		__attribute__((fallthrough));
+	case 8:
+		/* Removes LANDLOCK_ACCESS_FS_RESOLVE_UNIX for ABI < 9 */
+		ruleset_attr.handled_access_fs &=
+			~LANDLOCK_ACCESS_FS_RESOLVE_UNIX;
+		__attribute__((fallthrough));
 	case LANDLOCK_ABI_LAST:
 		break;
 	default:
Signature · 1 line
2.52.0
maintainer
#28 lore raw

Re: [PATCH v5 3/9] samples/landlock: Add support for named UNIX domain socket restrictions

Quoted 42 lines · Günther Noack

The access right for UNIX domain socket lookups is grouped with the read-write rights in the sample tool. Rationale: In the general case, any operations are possible through a UNIX domain socket, including data-mutating operations.

Cc Justin Suess <[email protected]>
Cc Mickaël Salaün <[email protected]>
---
 samples/landlock/sandboxer.c | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)
samples/landlock/sandboxer.c +5 -3
index e7af02f98208..0bbbc5c9ead6 100644
@@ -111,7 +111,8 @@ static int parse_path(char *env_path, const char ***const path_list)
 	LANDLOCK_ACCESS_FS_WRITE_FILE | \
 	LANDLOCK_ACCESS_FS_READ_FILE | \
 	LANDLOCK_ACCESS_FS_TRUNCATE | \
-	LANDLOCK_ACCESS_FS_IOCTL_DEV)
+	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
 
 /* clang-format on */
 
@@ -295,11 +296,12 @@ static bool check_ruleset_scope(const char *const env_var,
 	LANDLOCK_ACCESS_FS_MAKE_SYM | \
 	LANDLOCK_ACCESS_FS_REFER | \
 	LANDLOCK_ACCESS_FS_TRUNCATE | \
-	LANDLOCK_ACCESS_FS_IOCTL_DEV)
+	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
 
 /* clang-format on */
 
-#define LANDLOCK_ABI_LAST 7
+#define LANDLOCK_ABI_LAST 9
 
 #define XSTR(s) #s
 #define STR(s) XSTR(s)

samples/landlock/sandboxer.c int main(const int argc, char *const argv[], char *const *const envp) · line 450

Quoted 5 lines
samples/landlock/sandboxer.c +2 -0
@@ -444,6 +446,13 @@ int main(const int argc, char *const argv[], char *const *const envp)
 			"provided by ABI version %d (instead of %d).\n",
 			LANDLOCK_ABI_LAST, abi);
 		__attribute__((fallthrough));
+	case 7:
+		__attribute__((fallthrough));

The current code should print the hint when ABI <= 7. Please send a dedicated patch to fix the TSYNC-related changes.

Quoted 12 lines
samples/landlock/sandboxer.c +5 -0
+	case 8:
+		/* Removes LANDLOCK_ACCESS_FS_RESOLVE_UNIX for ABI < 9 */
+		ruleset_attr.handled_access_fs &=
+			~LANDLOCK_ACCESS_FS_RESOLVE_UNIX;
+		__attribute__((fallthrough));
 	case LANDLOCK_ABI_LAST:
 		break;
 	default:
Signature · 1 line
2.52.0
#29 lore raw

Re: [PATCH v5 3/9] samples/landlock: Add support for named UNIX domain socket restrictions

Quoted 10 lines · Mickaël Salaün

samples/landlock/sandboxer.c int main(const int argc, char *const argv[], char *const *const envp) · line 450

Quoted 6 lines · Günther Noack
samples/landlock/sandboxer.c +2 -0
@@ -444,6 +446,13 @@ int main(const int argc, char *const argv[], char *const *const envp)
 			"provided by ABI version %d (instead of %d).\n",
 			LANDLOCK_ABI_LAST, abi);
 		__attribute__((fallthrough));
+	case 7:
+		__attribute__((fallthrough));

The current code should print the hint when ABI <= 7. Please send a dedicated patch to fix the TSYNC-related changes.

Good catch, thanks! I sent a follow-up.

https://lore.kernel.org/all/[email protected]/

–Günther

#30 lore raw

PATCH v5 4/9 landlock/selftests: Test LANDLOCK_ACCESS_FS_RESOLVE_UNIX

In reply to Günther Noack

* Extract common helpers from an existing IOCTL test that also uses pathname unix(7) sockets.
* These tests use the common scoped domains fixture which is also used in other Landlock scoping tests and which was used in Tingmao Wang's earlier patch set in [1].

These tests exercise the cross product of the following scenarios:

* Stream connect(), Datagram connect(), Datagram sendmsg() and Seqpacket connect().
* Child-to-parent and parent-to-child communication
* The Landlock policy configuration as listed in the scoped_domains fixture.
* In the default variant, Landlock domains are only placed where prescribed in the fixture.
* In the "ALL_DOMAINS" variant, Landlock domains are also placed in the places where the fixture says to omit them, but with a LANDLOCK_RULE_PATH_BENEATH that allows connection.

Cc: Justin Suess <[email protected]>
Cc: Tingmao Wang <[email protected]>
Cc: Mickaël Salaün <[email protected]>
Link[1]: https://lore.kernel.org/all/53b9883648225d5a08e82d2636ab0b4fda003bc9.1767115163.git.m@maowtm.org/

---
 tools/testing/selftests/landlock/fs_test.c | 384 ++++++++++++++++++++-
 1 file changed, 368 insertions(+), 16 deletions(-)
tools/testing/selftests/landlock/fs_test.c +368 -16
index b318627e7561..bdeff2e0e029 100644
@@ -4358,30 +4358,61 @@ TEST_F_FORK(layout1, named_pipe_ioctl)
 	ASSERT_EQ(child_pid, waitpid(child_pid, NULL, 0));
 }
 
+/*
+ * set_up_named_unix_server - Create a pathname unix socket
+ *
+ * If the socket type is not SOCK_DGRAM, also invoke listen(2).
+ *
+ * Return: The listening FD - it is the caller responsibility to close it.
+ */
+static int set_up_named_unix_server(struct __test_metadata *const _metadata,
+				    int type, const char *const path)
+{
+	int fd;
+	struct sockaddr_un addr = {
+		.sun_family = AF_UNIX,
+	};
+
+	fd = socket(AF_UNIX, type, 0);
+	ASSERT_LE(0, fd);
+
+	strncpy(addr.sun_path, path, sizeof(addr.sun_path));
+	ASSERT_EQ(0, bind(fd, (struct sockaddr *)&addr, sizeof(addr)));
+
+	if (type != SOCK_DGRAM)
+		ASSERT_EQ(0, listen(fd, 10 /* qlen */));
+	return fd;
+}
+
+/*
+ * test_connect_named_unix - connect to the given named UNIX socket
+ *
+ * Return: The errno from connect(), or 0
+ */
+static int test_connect_named_unix(int fd, const char *const path)
+{
+	struct sockaddr_un addr = {
+		.sun_family = AF_UNIX,
+	};
+	strncpy(addr.sun_path, path, sizeof(addr.sun_path));
+
+	if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
+		return errno;
+	return 0;
+}
+
 /* For named UNIX domain sockets, no IOCTL restrictions apply. */
 TEST_F_FORK(layout1, named_unix_domain_socket_ioctl)
 {
 	const char *const path = file1_s1d1;
 	int srv_fd, cli_fd, ruleset_fd;
-	struct sockaddr_un srv_un = {
-		.sun_family = AF_UNIX,
-	};
-	struct sockaddr_un cli_un = {
-		.sun_family = AF_UNIX,
-	};
 	const struct landlock_ruleset_attr attr = {
 		.handled_access_fs = LANDLOCK_ACCESS_FS_IOCTL_DEV,
 	};
 
 	/* Sets up a server */
 	ASSERT_EQ(0, unlink(path));
-	srv_fd = socket(AF_UNIX, SOCK_STREAM, 0);
-	ASSERT_LE(0, srv_fd);
-
-	strncpy(srv_un.sun_path, path, sizeof(srv_un.sun_path));
-	ASSERT_EQ(0, bind(srv_fd, (struct sockaddr *)&srv_un, sizeof(srv_un)));
-
-	ASSERT_EQ(0, listen(srv_fd, 10 /* qlen */));
+	srv_fd = set_up_named_unix_server(_metadata, SOCK_STREAM, path);
 
 	/* Enables Landlock. */
 	ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
@@ -4393,9 +4424,7 @@ TEST_F_FORK(layout1, named_unix_domain_socket_ioctl)
 	cli_fd = socket(AF_UNIX, SOCK_STREAM, 0);
 	ASSERT_LE(0, cli_fd);
 
-	strncpy(cli_un.sun_path, path, sizeof(cli_un.sun_path));
-	ASSERT_EQ(0,
-		  connect(cli_fd, (struct sockaddr *)&cli_un, sizeof(cli_un)));
+	ASSERT_EQ(0, test_connect_named_unix(cli_fd, path));
 
 	/* FIONREAD and other IOCTLs should not be forbidden. */
 	EXPECT_EQ(0, test_fionread_ioctl(cli_fd));
@@ -4570,6 +4599,329 @@ TEST_F_FORK(ioctl, handle_file_access_file)
 	ASSERT_EQ(0, close(file_fd));
 }
 
+/*
+ * test_sendto_named_unix - sendto to the given named UNIX socket
+ *
+ * sendto() is equivalent to sendmsg() in this respect.
+ *
+ * Return: The errno from sendto(), or 0
+ */
+static int test_sendto_named_unix(int fd, const char *const path)
+{
+	static const char buf[] = "dummy";
+	struct sockaddr_un addr = {
+		.sun_family = AF_UNIX,
+	};
+	strncpy(addr.sun_path, path, sizeof(addr.sun_path));
+
+	if (sendto(fd, buf, sizeof(buf), 0, (struct sockaddr *)&addr,
+		   sizeof(addr)) == -1)
+		return errno;
+	return 0;
+}
+
+/* clang-format off */
+FIXTURE(scoped_domains) {};
+/* clang-format on */
+
+#include "scoped_base_variants.h"
+
+FIXTURE_SETUP(scoped_domains)
+{
+	drop_caps(_metadata);
+};
+
+FIXTURE_TEARDOWN(scoped_domains)
+{
+}
+
+static void enforce_fs_resolve_unix(struct __test_metadata *const _metadata,
+				    const struct rule rules[])
+{
+	if (rules) {
+		int fd = create_ruleset(_metadata,
+					LANDLOCK_ACCESS_FS_RESOLVE_UNIX, rules);
+		enforce_ruleset(_metadata, fd);
+		EXPECT_EQ(0, close(fd));
+	} else {
+		drop_access_rights(
+			_metadata,
+			&(struct landlock_ruleset_attr){
+				.handled_access_fs =
+					LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+			});
+	}
+}
+
+/*
+ * Flags for test_connect_to_parent and test_connect_to_child:
+ *
+ * USE_SENDTO: Use sendto() instead of connect() (for SOCK_DGRAM only)
+ * ENFORCE_ALL: Enforce a Landlock domain even when the variant says
+ *   we shouldn't.  We enforce a domain where the path is allow-listed,
+ *   and expect the behavior to be the same as if none was used.
+ */
+#define USE_SENDTO (1 << 0)
+#define ENFORCE_ALL (1 << 1)
+
+static void test_connect_to_parent(struct __test_metadata *const _metadata,
+				   const FIXTURE_VARIANT(scoped_domains) *
+					   variant,
+				   int sock_type, int flags)
+{
+	const char *const path = "sock";
+	const struct rule rules[] = {
+		{
+			.path = ".",
+			.access = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+		},
+		{},
+	};
+	int cli_fd, srv_fd, res, status;
+	pid_t child_pid;
+	int readiness_pipe[2];
+	char buf[1];
+
+	if (variant->domain_both)
+		enforce_fs_resolve_unix(_metadata, NULL);
+	else if (flags & ENFORCE_ALL)
+		enforce_fs_resolve_unix(_metadata, rules);
+
+	unlink(path);
+	ASSERT_EQ(0, pipe2(readiness_pipe, O_CLOEXEC));
+
+	child_pid = fork();
+	ASSERT_LE(0, child_pid);
+
+	if (child_pid == 0) {
+		if (variant->domain_child)
+			enforce_fs_resolve_unix(_metadata, NULL);
+		else if (flags & ENFORCE_ALL)
+			enforce_fs_resolve_unix(_metadata, rules);
+
+		/* Wait for server to be available. */
+		EXPECT_EQ(0, close(readiness_pipe[1]));
+		EXPECT_EQ(1, read(readiness_pipe[0], &buf, 1));
+		EXPECT_EQ(0, close(readiness_pipe[0]));
+
+		/* Talk to server. */
+		cli_fd = socket(AF_UNIX, sock_type, 0);
+		ASSERT_LE(0, cli_fd);
+
+		if (flags & USE_SENDTO)
+			res = test_sendto_named_unix(cli_fd, path);
+		else
+			res = test_connect_named_unix(cli_fd, path);
+
+		EXPECT_EQ(variant->domain_child ? EACCES : 0, res);
+
+		/* Clean up. */
+		EXPECT_EQ(0, close(cli_fd));
+
+		_exit(_metadata->exit_code);
+		return;
+	}
+
+	if (variant->domain_parent)
+		enforce_fs_resolve_unix(_metadata, NULL);
+	else if (flags & ENFORCE_ALL)
+		enforce_fs_resolve_unix(_metadata, rules);
+
+	srv_fd = set_up_named_unix_server(_metadata, sock_type, path);
+
+	/* Tell the child that it can connect. */
+	EXPECT_EQ(0, close(readiness_pipe[0]));
+	EXPECT_EQ(sizeof(buf), write(readiness_pipe[1], buf, sizeof(buf)));
+	EXPECT_EQ(0, close(readiness_pipe[1]));
+
+	/* Wait for child. */
+	ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
+	EXPECT_EQ(1, WIFEXITED(status));
+	EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+
+	/* Clean up. */
+	EXPECT_EQ(0, close(srv_fd));
+	EXPECT_EQ(0, unlink(path));
+}
+
+static void test_connect_to_child(struct __test_metadata *const _metadata,
+				  const FIXTURE_VARIANT(scoped_domains) *
+					  variant,
+				  int sock_type, int flags)
+{
+	const char *const path = "sock";
+	const struct rule rules[] = {
+		{
+			.path = ".",
+			.access = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+		},
+		{},
+	};
+	int readiness_pipe[2];
+	int shutdown_pipe[2];
+	int cli_fd, srv_fd, res, status;
+	pid_t child_pid;
+	char buf[1];
+
+	if (variant->domain_both)
+		enforce_fs_resolve_unix(_metadata, NULL);
+	else if (flags & ENFORCE_ALL)
+		enforce_fs_resolve_unix(_metadata, rules);
+
+	unlink(path);
+	ASSERT_EQ(0, pipe2(readiness_pipe, O_CLOEXEC));
+	ASSERT_EQ(0, pipe2(shutdown_pipe, O_CLOEXEC));
+
+	child_pid = fork();
+	ASSERT_LE(0, child_pid);
+
+	if (child_pid == 0) {
+		if (variant->domain_child)
+			enforce_fs_resolve_unix(_metadata, NULL);
+		else if (flags & ENFORCE_ALL)
+			enforce_fs_resolve_unix(_metadata, rules);
+
+		srv_fd = set_up_named_unix_server(_metadata, sock_type, path);
+
+		/* Tell the parent that it can connect. */
+		EXPECT_EQ(0, close(readiness_pipe[0]));
+		EXPECT_EQ(sizeof(buf),
+			  write(readiness_pipe[1], buf, sizeof(buf)));
+		EXPECT_EQ(0, close(readiness_pipe[1]));
+
+		/* Wait until it is time to shut down. */
+		EXPECT_EQ(0, close(shutdown_pipe[1]));
+		EXPECT_EQ(1, read(shutdown_pipe[0], &buf, 1));
+		EXPECT_EQ(0, close(shutdown_pipe[0]));
+
+		/* Cleanup */
+		EXPECT_EQ(0, close(srv_fd));
+		EXPECT_EQ(0, unlink(path));
+
+		_exit(_metadata->exit_code);
+		return;
+	}
+
+	if (variant->domain_parent)
+		enforce_fs_resolve_unix(_metadata, NULL);
+	else if (flags & ENFORCE_ALL)
+		enforce_fs_resolve_unix(_metadata, rules);
+
+	/* Wait for server to be available. */
+	EXPECT_EQ(0, close(readiness_pipe[1]));
+	EXPECT_EQ(1, read(readiness_pipe[0], &buf, 1));
+	EXPECT_EQ(0, close(readiness_pipe[0]));
+
+	/* Talk to server. */
+	cli_fd = socket(AF_UNIX, sock_type, 0);
+	ASSERT_LE(0, cli_fd);
+
+	if (flags & USE_SENDTO)
+		res = test_sendto_named_unix(cli_fd, path);
+	else
+		res = test_connect_named_unix(cli_fd, path);
+
+	EXPECT_EQ(variant->domain_parent ? EACCES : 0, res);
+
+	/* Clean up. */
+	EXPECT_EQ(0, close(cli_fd));
+
+	/* Tell the server to shut down. */
+	EXPECT_EQ(0, close(shutdown_pipe[0]));
+	EXPECT_EQ(sizeof(buf), write(shutdown_pipe[1], buf, sizeof(buf)));
+	EXPECT_EQ(0, close(shutdown_pipe[1]));
+
+	/* Wait for child. */
+	ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
+	EXPECT_EQ(1, WIFEXITED(status));
+	EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+}
+
+TEST_F(scoped_domains, unix_stream_connect_to_parent)
+{
+	test_connect_to_parent(_metadata, variant, SOCK_STREAM, 0);
+}
+
+TEST_F(scoped_domains, unix_dgram_connect_to_parent)
+{
+	test_connect_to_parent(_metadata, variant, SOCK_DGRAM, 0);
+}
+
+TEST_F(scoped_domains, unix_dgram_sendmsg_to_parent)
+{
+	test_connect_to_parent(_metadata, variant, SOCK_DGRAM, USE_SENDTO);
+}
+
+TEST_F(scoped_domains, unix_seqpacket_connect_to_parent)
+{
+	test_connect_to_parent(_metadata, variant, SOCK_SEQPACKET, 0);
+}
+
+TEST_F(scoped_domains, unix_stream_connect_to_parent_full)
+{
+	test_connect_to_parent(_metadata, variant, SOCK_STREAM, ENFORCE_ALL);
+}
+
+TEST_F(scoped_domains, unix_dgram_connect_to_parent_full)
+{
+	test_connect_to_parent(_metadata, variant, SOCK_DGRAM, ENFORCE_ALL);
+}
+
+TEST_F(scoped_domains, unix_dgram_sendmsg_to_parent_full)
+{
+	test_connect_to_parent(_metadata, variant, SOCK_DGRAM,
+			       USE_SENDTO | ENFORCE_ALL);
+}
+
+TEST_F(scoped_domains, unix_seqpacket_connect_to_parent_full)
+{
+	test_connect_to_parent(_metadata, variant, SOCK_SEQPACKET, ENFORCE_ALL);
+}
+
+TEST_F(scoped_domains, unix_stream_connect_to_child)
+{
+	test_connect_to_child(_metadata, variant, SOCK_STREAM, 0);
+}
+
+TEST_F(scoped_domains, unix_dgram_connect_to_child)
+{
+	test_connect_to_child(_metadata, variant, SOCK_DGRAM, 0);
+}
+
+TEST_F(scoped_domains, unix_dgram_sendmsg_to_child)
+{
+	test_connect_to_child(_metadata, variant, SOCK_DGRAM, USE_SENDTO);
+}
+
+TEST_F(scoped_domains, unix_seqpacket_connect_to_child)
+{
+	test_connect_to_child(_metadata, variant, SOCK_SEQPACKET, 0);
+}
+
+TEST_F(scoped_domains, unix_stream_connect_to_child_full)
+{
+	test_connect_to_child(_metadata, variant, SOCK_STREAM, ENFORCE_ALL);
+}
+
+TEST_F(scoped_domains, unix_dgram_connect_to_child_full)
+{
+	test_connect_to_child(_metadata, variant, SOCK_DGRAM, ENFORCE_ALL);
+}
+
+TEST_F(scoped_domains, unix_dgram_sendmsg_to_child_full)
+{
+	test_connect_to_child(_metadata, variant, SOCK_DGRAM,
+			      USE_SENDTO | ENFORCE_ALL);
+}
+
+TEST_F(scoped_domains, unix_seqpacket_connect_to_child_full)
+{
+	test_connect_to_child(_metadata, variant, SOCK_SEQPACKET, ENFORCE_ALL);
+}
+
+#undef USE_SENDTO
+#undef ENFORCE_ALL
+
 /* clang-format off */
 FIXTURE(layout1_bind) {};
 /* clang-format on */
Signature · 1 line
2.52.0
maintainer
#31 lore raw

Re: [PATCH v5 4/9] landlock/selftests: Test LANDLOCK_ACCESS_FS_RESOLVE_UNIX

Quoted 45 lines · Günther Noack

* Extract common helpers from an existing IOCTL test that also uses pathname unix(7) sockets.
* These tests use the common scoped domains fixture which is also used in other Landlock scoping tests and which was used in Tingmao Wang's earlier patch set in [1].

These tests exercise the cross product of the following scenarios:

* Stream connect(), Datagram connect(), Datagram sendmsg() and Seqpacket connect().
* Child-to-parent and parent-to-child communication
* The Landlock policy configuration as listed in the scoped_domains fixture.
* In the default variant, Landlock domains are only placed where prescribed in the fixture.
* In the "ALL_DOMAINS" variant, Landlock domains are also placed in the places where the fixture says to omit them, but with a LANDLOCK_RULE_PATH_BENEATH that allows connection.

Cc: Justin Suess <[email protected]>
Cc: Tingmao Wang <[email protected]>
Cc: Mickaël Salaün <[email protected]>
Link[1]: https://lore.kernel.org/all/53b9883648225d5a08e82d2636ab0b4fda003bc9.1767115163.git.m@maowtm.org/

---
 tools/testing/selftests/landlock/fs_test.c | 384 ++++++++++++++++++++-
 1 file changed, 368 insertions(+), 16 deletions(-)
tools/testing/selftests/landlock/fs_test.c +9 -0
index b318627e7561..bdeff2e0e029 100644
@@ -4358,30 +4358,61 @@ TEST_F_FORK(layout1, named_pipe_ioctl)
 	ASSERT_EQ(child_pid, waitpid(child_pid, NULL, 0));
 }
 
+/*
+ * set_up_named_unix_server - Create a pathname unix socket
+ *
+ * If the socket type is not SOCK_DGRAM, also invoke listen(2).
+ *
+ * Return: The listening FD - it is the caller responsibility to close it.
+ */
+static int set_up_named_unix_server(struct __test_metadata *const _metadata,
+				    int type, const char *const path)

tools/testing/selftests/landlock/fs_test.c TEST_F_FORK(layout1, named_pipe_ioctl) · line 4379

Quoted 10 lines
tools/testing/selftests/landlock/fs_test.c +10 -0
+{
+	int fd;
+	struct sockaddr_un addr = {
+		.sun_family = AF_UNIX,
+	};
+
+	fd = socket(AF_UNIX, type, 0);
+	ASSERT_LE(0, fd);
+
+	strncpy(addr.sun_path, path, sizeof(addr.sun_path));

fs_test.c: In function ‘set_up_named_unix_server’:
fs_test.c:4125:9: error: ‘strncpy’ specified bound 108 equals destination size [-Werror=stringop-truncation]

 4125 |         strncpy(addr.sun_path, path, sizeof(addr.sun_path));
      |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

We should also ASSERT the result to make sure path's length is not too big.

Quoted 6 lines
tools/testing/selftests/landlock/fs_test.c +6 -0
+	ASSERT_EQ(0, bind(fd, (struct sockaddr *)&addr, sizeof(addr)));
+
+	if (type != SOCK_DGRAM)
+		ASSERT_EQ(0, listen(fd, 10 /* qlen */));
+	return fd;
+}
#32 lore raw

Re: [PATCH v5 4/9] landlock/selftests: Test LANDLOCK_ACCESS_FS_RESOLVE_UNIX

Quoted 4 lines · Mickaël Salaün
Quoted 2 lines · Günther Noack
tools/testing/selftests/landlock/fs_test.c +0 -0
Quoted 8 lines

tools/testing/selftests/landlock/fs_test.c

Quoted 1 line
tools/testing/selftests/landlock/fs_test.c +1 -0
+	strncpy(addr.sun_path, path, sizeof(addr.sun_path));

fs_test.c: In function ‘set_up_named_unix_server’:
fs_test.c:4125:9: error: ‘strncpy’ specified bound 108 equals destination size [-Werror=stringop-truncation]

 4125 |         strncpy(addr.sun_path, path, sizeof(addr.sun_path));
      |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

We should also ASSERT the result to make sure path's length is not too big.

Fair enough, will do, because it's less confusing.

(FWIW though, I think Linux can technically deal with a sun_path that does not end in a NUL byte. See the long comment in unix_mkname_bsd(). But that's a Linux peculiarity.)

–Günther

#33 lore raw

Re: [PATCH v5 4/9] landlock/selftests: Test LANDLOCK_ACCESS_FS_RESOLVE_UNIX

Quoted 12 lines · Günther Noack
Quoted 11 lines · Mickaël Salaün

tools/testing/selftests/landlock/fs_test.c

Quoted 4 lines · Günther Noack
tools/testing/selftests/landlock/fs_test.c +1 -0
 
+	strncpy(addr.sun_path, path, sizeof(addr.sun_path));

fs_test.c: In function ‘set_up_named_unix_server’:
fs_test.c:4125:9: error: ‘strncpy’ specified bound 108 equals destination size [-Werror=stringop-truncation]

 4125 |         strncpy(addr.sun_path, path, sizeof(addr.sun_path));
      |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Quoted 7 lines
Quoted 1 line

We should also ASSERT the result to make sure path's length is not too big.

Fair enough, will do, because it's less confusing.

(FWIW though, I think Linux can technically deal with a sun_path that does not end in a NUL byte. See the long comment in unix_mkname_bsd(). But that's a Linux peculiarity.)

FYI, before the question comes up why I didn't use strscpy: We don't have that library in these userspace tests, so it is now:

	struct sockaddr_un addr = {
        	.sun_family = AF_UNIX,
        };

	ASSERT_LT(strlen(path), sizeof(addr.sun_path));
	strncpy(addr.sun_path, path, sizeof(addr.sun_path)-1);

–Günther

#34 lore raw

PATCH v5 5/9 landlock/selftests: Audit test for LANDLOCK_ACCESS_FS_RESOLVE_UNIX

In reply to Günther Noack

Add an audit test to check that Landlock denials from LANDLOCK_ACCESS_FS_RESOLVE_UNIX result in audit logs in the expected format. (There is one audit test for each filesystem access right, so we should add one for LANDLOCK_ACCESS_FS_RESOLVE_UNIX as well.)

---
 tools/testing/selftests/landlock/fs_test.c | 42 +++++++++++++++++++++-
 1 file changed, 41 insertions(+), 1 deletion(-)
tools/testing/selftests/landlock/fs_test.c +41 -1
index bdeff2e0e029..8fa9d7c49ac3 100644
@@ -7530,7 +7530,8 @@ static const __u64 access_fs_16 =
 	LANDLOCK_ACCESS_FS_MAKE_SYM |
 	LANDLOCK_ACCESS_FS_REFER |
 	LANDLOCK_ACCESS_FS_TRUNCATE |
-	LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	LANDLOCK_ACCESS_FS_IOCTL_DEV |
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX;
 /* clang-format on */
 
 TEST_F(audit_layout1, execute_read)
@@ -7975,6 +7976,45 @@ TEST_F(audit_layout1, ioctl_dev)
 	EXPECT_EQ(1, records.domain);
 }
 
+TEST_F(audit_layout1, resolve_unix)
+{
+	struct audit_records records;
+	const char *const path = "sock";
+	int srv_fd, cli_fd, status;
+	pid_t child_pid;
+
+	srv_fd = set_up_named_unix_server(_metadata, SOCK_STREAM, path);
+
+	child_pid = fork();
+	ASSERT_LE(0, child_pid);
+	if (!child_pid) {
+		drop_access_rights(_metadata,
+				   &(struct landlock_ruleset_attr){
+					   .handled_access_fs = access_fs_16,
+				   });
+
+		cli_fd = socket(AF_UNIX, SOCK_STREAM, 0);
+		ASSERT_LE(0, cli_fd);
+		EXPECT_EQ(EACCES, test_connect_named_unix(cli_fd, path));
+
+		EXPECT_EQ(0, close(cli_fd));
+		_exit(_metadata->exit_code);
+	}
+
+	ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
+	EXPECT_EQ(1, WIFEXITED(status));
+	EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+
+	EXPECT_EQ(0, matches_log_fs_extra(_metadata, self->audit_fd,
+					  "fs\\.resolve_unix", path, NULL));
+
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	EXPECT_EQ(0, records.access);
+	EXPECT_EQ(1, records.domain);
+
+	EXPECT_EQ(0, close(srv_fd));
+}
+
 TEST_F(audit_layout1, mount)
 {
 	struct audit_records records;
Signature · 1 line
2.52.0
#35 lore raw

PATCH v5 6/9 landlock/selftests: Check that coredump sockets stay unrestricted

In reply to Günther Noack

Even when a process is restricted with the new LANDLOCK_ACCESS_FS_RESOLVE_SOCKET right, the kernel can continue writing its coredump to the configured coredump socket.

In the test, we create a local server and rewire the system to write coredumps into it. We then create a child process within a Landlock domain where LANDLOCK_ACCESS_FS_RESOLVE_SOCKET is restricted and make the process crash. The test uses SO_PEERCRED to check that the connecting client process is the expected one.

---
 tools/testing/selftests/landlock/fs_test.c | 122 +++++++++++++++++++++
 1 file changed, 122 insertions(+)
tools/testing/selftests/landlock/fs_test.c +122 -0
index 8fa9d7c49ac3..705d8a13d2e0 100644
@@ -22,6 +22,7 @@
 #include <sys/ioctl.h>
 #include <sys/mount.h>
 #include <sys/prctl.h>
+#include <sys/resource.h>
 #include <sys/sendfile.h>
 #include <sys/socket.h>
 #include <sys/stat.h>
@@ -4922,6 +4923,127 @@ TEST_F(scoped_domains, unix_seqpacket_connect_to_child_full)
 #undef USE_SENDTO
 #undef ENFORCE_ALL
 
+static void read_core_pattern(struct __test_metadata *const _metadata,
+			      char *buf, size_t buf_size)
+{
+	int fd;
+	ssize_t ret;
+
+	fd = open("/proc/sys/kernel/core_pattern", O_RDONLY | O_CLOEXEC);
+	ASSERT_LE(0, fd);
+
+	ret = read(fd, buf, buf_size - 1);
+	ASSERT_LE(0, ret);
+	EXPECT_EQ(0, close(fd));
+
+	buf[ret] = '\0';
+}
+
+static void set_core_pattern(struct __test_metadata *const _metadata,
+			     const char *pattern)
+{
+	int fd;
+	size_t len = strlen(pattern);
+
+	fd = open("/proc/sys/kernel/core_pattern", O_WRONLY | O_CLOEXEC);
+	ASSERT_LE(0, fd);
+
+	ASSERT_EQ(len, write(fd, pattern, len));
+	EXPECT_EQ(0, close(fd));
+}
+
+FIXTURE(coredump)
+{
+	char original_core_pattern[256];
+};
+
+FIXTURE_SETUP(coredump)
+{
+	disable_caps(_metadata);
+	read_core_pattern(_metadata, self->original_core_pattern,
+			  sizeof(self->original_core_pattern));
+}
+
+FIXTURE_TEARDOWN_PARENT(coredump)
+{
+	set_cap(_metadata, CAP_SYS_ADMIN);
+	set_core_pattern(_metadata, self->original_core_pattern);
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/*
+ * Test that even when a process is restricted with
+ * LANDLOCK_ACCESS_FS_RESOLVE_UNIX, the kernel can still initiate a connection
+ * to the coredump socket on the processes' behalf.
+ */
+TEST_F_FORK(coredump, socket_not_restricted)
+{
+	static const char core_pattern[] = "@/tmp/landlock_coredump_test.sock";
+	const char *const sock_path = core_pattern + 1;
+	int srv_fd, conn_fd, status;
+	pid_t child_pid;
+	struct ucred cred;
+	socklen_t cred_len = sizeof(cred);
+	char buf[4096];
+
+	/* Set up the coredump server socket. */
+	unlink(sock_path);
+	srv_fd = set_up_named_unix_server(_metadata, SOCK_STREAM, sock_path);
+
+	/* Point coredumps at our socket. */
+	set_cap(_metadata, CAP_SYS_ADMIN);
+	set_core_pattern(_metadata, core_pattern);
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+
+	/* Restrict LANDLOCK_ACCESS_FS_RESOLVE_UNIX. */
+	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
+		.handled_access_fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+	});
+
+	/* Fork a child that crashes. */
+	child_pid = fork();
+	ASSERT_LE(0, child_pid);
+	if (child_pid == 0) {
+		struct rlimit rl = {
+			.rlim_cur = RLIM_INFINITY,
+			.rlim_max = RLIM_INFINITY,
+		};
+
+		ASSERT_EQ(0, setrlimit(RLIMIT_CORE, &rl));
+
+		/* Crash on purpose. */
+		kill(getpid(), SIGSEGV);
+		_exit(1);
+	}
+
+	/*
+	 * Accept the coredump connection.  If Landlock incorrectly denies the
+	 * kernel's coredump connect, accept() will block forever, so the test
+	 * would time out.
+	 */
+	conn_fd = accept(srv_fd, NULL, NULL);
+	ASSERT_LE(0, conn_fd);
+
+	/* Check that the connection came from the crashing child. */
+	ASSERT_EQ(0, getsockopt(conn_fd, SOL_SOCKET, SO_PEERCRED, &cred,
+				&cred_len));
+	EXPECT_EQ(child_pid, cred.pid);
+
+	/* Drain the coredump data so the kernel can finish. */
+	while (read(conn_fd, buf, sizeof(buf)) > 0)
+		;
+
+	EXPECT_EQ(0, close(conn_fd));
+
+	/* Wait for the child and verify it coredumped. */
+	ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
+	ASSERT_TRUE(WIFSIGNALED(status));
+	ASSERT_TRUE(WCOREDUMP(status));
+
+	EXPECT_EQ(0, close(srv_fd));
+	EXPECT_EQ(0, unlink(sock_path));
+}
+
 /* clang-format off */
 FIXTURE(layout1_bind) {};
 /* clang-format on */
Signature · 1 line
2.52.0
maintainer
#36 lore raw

Re: [PATCH v5 6/9] landlock/selftests: Check that coredump sockets stay unrestricted

Quoted 49 lines · Günther Noack

Even when a process is restricted with the new LANDLOCK_ACCESS_FS_RESOLVE_SOCKET right, the kernel can continue writing its coredump to the configured coredump socket.

In the test, we create a local server and rewire the system to write coredumps into it. We then create a child process within a Landlock domain where LANDLOCK_ACCESS_FS_RESOLVE_SOCKET is restricted and make the process crash. The test uses SO_PEERCRED to check that the connecting client process is the expected one.

---
 tools/testing/selftests/landlock/fs_test.c | 122 +++++++++++++++++++++
 1 file changed, 122 insertions(+)
tools/testing/selftests/landlock/fs_test.c +19 -0
index 8fa9d7c49ac3..705d8a13d2e0 100644
@@ -22,6 +22,7 @@
 #include <sys/ioctl.h>
 #include <sys/mount.h>
 #include <sys/prctl.h>
+#include <sys/resource.h>
 #include <sys/sendfile.h>
 #include <sys/socket.h>
 #include <sys/stat.h>
@@ -4922,6 +4923,127 @@ TEST_F(scoped_domains, unix_seqpacket_connect_to_child_full)
 #undef USE_SENDTO
 #undef ENFORCE_ALL
 
+static void read_core_pattern(struct __test_metadata *const _metadata,
+			      char *buf, size_t buf_size)
+{
+	int fd;
+	ssize_t ret;
+
+	fd = open("/proc/sys/kernel/core_pattern", O_RDONLY | O_CLOEXEC);
+	ASSERT_LE(0, fd);
+
+	ret = read(fd, buf, buf_size - 1);
+	ASSERT_LE(0, ret);
+	EXPECT_EQ(0, close(fd));
+
+	buf[ret] = '\0';
+}
+
+static void set_core_pattern(struct __test_metadata *const _metadata,
+			     const char *pattern)

tools/testing/selftests/landlock/fs_test.c TEST_F(scoped_domains, unix_seqpacket_connect_to_child_full) · line 4953

Quoted 10 lines
tools/testing/selftests/landlock/fs_test.c +10 -0
+{
+	int fd;
+	size_t len = strlen(pattern);
+
+	fd = open("/proc/sys/kernel/core_pattern", O_WRONLY | O_CLOEXEC);
+	ASSERT_LE(0, fd);
+
+	ASSERT_EQ(len, write(fd, pattern, len));
+	EXPECT_EQ(0, close(fd));
+}

I had to fix this helper to make it work with check-linux.sh:

tools/testing/selftests/landlock/fs_test.c +24 -5
index ae32513fb54b..64887d34079a 100644
@@ -4659,11 +4659,34 @@ static void set_core_pattern(struct __test_metadata *const _metadata,
 	int fd;
 	size_t len = strlen(pattern);
 
+	/*
+	 * Writing to /proc/sys/kernel/core_pattern requires EUID 0 because
+	 * sysctl_perm() checks that, ignoring capabilities like
+	 * CAP_SYS_ADMIN or CAP_DAC_OVERRIDE.
+	 *
+	 * Switching EUID clears the dumpable flag, which must be restored
+	 * afterwards to allow coredumps.
+	 */
+	set_cap(_metadata, CAP_SETUID);
+	ASSERT_EQ(0, seteuid(0));
+	clear_cap(_metadata, CAP_SETUID);
+
 	fd = open("/proc/sys/kernel/core_pattern", O_WRONLY | O_CLOEXEC);
-	ASSERT_LE(0, fd);
+	ASSERT_LE(0, fd)
+	{
+		TH_LOG("Failed to open core_pattern for writing: %s",
+		       strerror(errno));
+	}
 
 	ASSERT_EQ(len, write(fd, pattern, len));
 	EXPECT_EQ(0, close(fd));
+
+	set_cap(_metadata, CAP_SETUID);
+	ASSERT_EQ(0, seteuid(getuid()));
+	clear_cap(_metadata, CAP_SETUID);
+
+	/* Restore dumpable flag cleared by seteuid(). */
+	ASSERT_EQ(0, prctl(PR_SET_DUMPABLE, 1 , 0, 0, 0));
 }
 
 FIXTURE(coredump)
@@ -4680,9 +4703,7 @@ FIXTURE_SETUP(coredump)
 
 FIXTURE_TEARDOWN_PARENT(coredump)
 {
-	set_cap(_metadata, CAP_SYS_ADMIN);
 	set_core_pattern(_metadata, self->original_core_pattern);
-	clear_cap(_metadata, CAP_SYS_ADMIN);
 }
 
 /*
@@ -4705,9 +4726,7 @@ TEST_F_FORK(coredump, socket_not_restricted)
 	srv_fd = set_up_named_unix_server(_metadata, SOCK_STREAM, sock_path);
 
 	/* Point coredumps at our socket. */
-	set_cap(_metadata, CAP_SYS_ADMIN);
 	set_core_pattern(_metadata, core_pattern);
-	clear_cap(_metadata, CAP_SYS_ADMIN);
 
 	/* Restrict LANDLOCK_ACCESS_FS_RESOLVE_UNIX. */
 	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, NULL);

Please run tests in this (minimal) environment.

Quoted 100 lines
tools/testing/selftests/landlock/fs_test.c +93 -0
+
+FIXTURE(coredump)
+{
+	char original_core_pattern[256];
+};
+
+FIXTURE_SETUP(coredump)
+{
+	disable_caps(_metadata);
+	read_core_pattern(_metadata, self->original_core_pattern,
+			  sizeof(self->original_core_pattern));
+}
+
+FIXTURE_TEARDOWN_PARENT(coredump)
+{
+	set_cap(_metadata, CAP_SYS_ADMIN);
+	set_core_pattern(_metadata, self->original_core_pattern);
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/*
+ * Test that even when a process is restricted with
+ * LANDLOCK_ACCESS_FS_RESOLVE_UNIX, the kernel can still initiate a connection
+ * to the coredump socket on the processes' behalf.
+ */
+TEST_F_FORK(coredump, socket_not_restricted)
+{
+	static const char core_pattern[] = "@/tmp/landlock_coredump_test.sock";
+	const char *const sock_path = core_pattern + 1;
+	int srv_fd, conn_fd, status;
+	pid_t child_pid;
+	struct ucred cred;
+	socklen_t cred_len = sizeof(cred);
+	char buf[4096];
+
+	/* Set up the coredump server socket. */
+	unlink(sock_path);
+	srv_fd = set_up_named_unix_server(_metadata, SOCK_STREAM, sock_path);
+
+	/* Point coredumps at our socket. */
+	set_cap(_metadata, CAP_SYS_ADMIN);
+	set_core_pattern(_metadata, core_pattern);
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+
+	/* Restrict LANDLOCK_ACCESS_FS_RESOLVE_UNIX. */
+	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
+		.handled_access_fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+	});
+
+	/* Fork a child that crashes. */
+	child_pid = fork();
+	ASSERT_LE(0, child_pid);
+	if (child_pid == 0) {
+		struct rlimit rl = {
+			.rlim_cur = RLIM_INFINITY,
+			.rlim_max = RLIM_INFINITY,
+		};
+
+		ASSERT_EQ(0, setrlimit(RLIMIT_CORE, &rl));
+
+		/* Crash on purpose. */
+		kill(getpid(), SIGSEGV);
+		_exit(1);
+	}
+
+	/*
+	 * Accept the coredump connection.  If Landlock incorrectly denies the
+	 * kernel's coredump connect, accept() will block forever, so the test
+	 * would time out.
+	 */
+	conn_fd = accept(srv_fd, NULL, NULL);
+	ASSERT_LE(0, conn_fd);
+
+	/* Check that the connection came from the crashing child. */
+	ASSERT_EQ(0, getsockopt(conn_fd, SOL_SOCKET, SO_PEERCRED, &cred,
+				&cred_len));
+	EXPECT_EQ(child_pid, cred.pid);
+
+	/* Drain the coredump data so the kernel can finish. */
+	while (read(conn_fd, buf, sizeof(buf)) > 0)
+		;
+
+	EXPECT_EQ(0, close(conn_fd));
+
+	/* Wait for the child and verify it coredumped. */
+	ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
+	ASSERT_TRUE(WIFSIGNALED(status));
+	ASSERT_TRUE(WCOREDUMP(status));
+
+	EXPECT_EQ(0, close(srv_fd));
+	EXPECT_EQ(0, unlink(sock_path));
+}
+
 /* clang-format off */
 FIXTURE(layout1_bind) {};
 /* clang-format on */
Signature · 1 line
2.52.0
#37 lore raw

PATCH v5 7/9 landlock/selftests: fs_test: Simplify ruleset creation and enforcement

In reply to Günther Noack

* Add enforce_fs() for defining and enforcing a ruleset in one step
* In some places, dropped "ASSERT_LE(0, fd)" checks after create_ruleset() call -- create_ruleset() already checks that.
* In some places, rename "file_fd" to "fd" if it is not needed to disambiguate any more.

---
 tools/testing/selftests/landlock/fs_test.c | 822 ++++++---------------
 1 file changed, 211 insertions(+), 611 deletions(-)
tools/testing/selftests/landlock/fs_test.c +211 -611
index 705d8a13d2e0..eb0058012f10 100644
@@ -767,15 +767,6 @@ static int create_ruleset(struct __test_metadata *const _metadata,
 		.handled_access_fs = handled_access_fs,
 	};
 
-	ASSERT_NE(NULL, rules)
-	{
-		TH_LOG("No rule list");
-	}
-	ASSERT_NE(NULL, rules[0].path)
-	{
-		TH_LOG("Empty rule list");
-	}
-
 	ruleset_fd =
 		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
 	ASSERT_LE(0, ruleset_fd)
@@ -783,16 +774,26 @@ static int create_ruleset(struct __test_metadata *const _metadata,
 		TH_LOG("Failed to create a ruleset: %s", strerror(errno));
 	}
 
-	for (i = 0; rules[i].path; i++) {
-		if (!rules[i].access)
-			continue;
+	if (rules)
+		for (i = 0; rules[i].path; i++) {
+			if (!rules[i].access)
+				continue;
 
-		add_path_beneath(_metadata, ruleset_fd, rules[i].access,
-				 rules[i].path);
-	}
+			add_path_beneath(_metadata, ruleset_fd, rules[i].access,
+					 rules[i].path);
+		}
 	return ruleset_fd;
 }
 
+static void enforce_fs(struct __test_metadata *const _metadata,
+		       const __u64 access_fs, const struct rule rules[])
+{
+	const int ruleset_fd = create_ruleset(_metadata, access_fs, rules);
+
+	enforce_ruleset(_metadata, ruleset_fd);
+	EXPECT_EQ(0, close(ruleset_fd));
+}
+
 TEST_F_FORK(layout0, proc_nsfs)
 {
 	const struct rule rules[] = {
@@ -879,13 +880,10 @@ TEST_F_FORK(layout1, effective_access)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 	char buf;
 	int reg_fd;
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	/* Tests on a directory (with or without O_PATH). */
 	ASSERT_EQ(EACCES, test_open("/", O_RDONLY));
@@ -932,12 +930,9 @@ TEST_F_FORK(layout1, unhandled_access)
 		},
 		{},
 	};
-	/* Here, we only handle read accesses, not write accesses. */
-	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RO, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	/* Here, we only handle read accesses, not write accesses. */
+	enforce_fs(_metadata, ACCESS_RO, rules);
 
 	/*
 	 * Because the policy does not handle LANDLOCK_ACCESS_FS_WRITE_FILE,
@@ -966,11 +961,8 @@ TEST_F_FORK(layout1, ruleset_overlap)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	/* Checks s1d1 hierarchy. */
 	ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
@@ -1022,11 +1014,8 @@ TEST_F_FORK(layout1, layer_rule_unions)
 		},
 		{},
 	};
-	int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer1);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer1);
 
 	/* Checks s1d1 hierarchy with layer1. */
 	ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
@@ -1048,10 +1037,7 @@ TEST_F_FORK(layout1, layer_rule_unions)
 	ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY));
 
 	/* Doesn't change anything from layer1. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer2);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer2);
 
 	/* Checks s1d1 hierarchy with layer2. */
 	ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
@@ -1073,10 +1059,7 @@ TEST_F_FORK(layout1, layer_rule_unions)
 	ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY));
 
 	/* Only allows write (but not read) to dir_s1d3. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer3);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer3);
 
 	/* Checks s1d1 hierarchy with layer3. */
 	ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
@@ -1114,27 +1097,18 @@ TEST_F_FORK(layout1, non_overlapping_accesses)
 		},
 		{},
 	};
-	int ruleset_fd;
 
 	ASSERT_EQ(0, unlink(file1_s1d1));
 	ASSERT_EQ(0, unlink(file1_s1d2));
 
-	ruleset_fd =
-		create_ruleset(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, layer1);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, layer1);
 
 	ASSERT_EQ(-1, mknod(file1_s1d1, S_IFREG | 0700, 0));
 	ASSERT_EQ(EACCES, errno);
 	ASSERT_EQ(0, mknod(file1_s1d2, S_IFREG | 0700, 0));
 	ASSERT_EQ(0, unlink(file1_s1d2));
 
-	ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_REMOVE_FILE,
-				    layer2);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_REMOVE_FILE, layer2);
 
 	/* Unchanged accesses for file creation. */
 	ASSERT_EQ(-1, mknod(file1_s1d1, S_IFREG | 0700, 0));
@@ -1238,37 +1212,24 @@ TEST_F_FORK(layout1, interleaved_masked_accesses)
 		},
 		{},
 	};
-	int ruleset_fd;
 
-	ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_FILE,
-				    layer1_read);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, layer1_read);
 
 	/* Checks that read access is granted for file1_s1d3 with layer 1. */
 	ASSERT_EQ(0, test_open(file1_s1d3, O_RDWR));
 	ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY));
 	ASSERT_EQ(0, test_open(file2_s1d3, O_WRONLY));
 
-	ruleset_fd = create_ruleset(_metadata,
-				    LANDLOCK_ACCESS_FS_READ_FILE |
-					    LANDLOCK_ACCESS_FS_WRITE_FILE,
-				    layer2_read_write);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE,
+		   layer2_read_write);
 
 	/* Checks that previous access rights are unchanged with layer 2. */
 	ASSERT_EQ(0, test_open(file1_s1d3, O_RDWR));
 	ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY));
 	ASSERT_EQ(0, test_open(file2_s1d3, O_WRONLY));
 
-	ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_FILE,
-				    layer3_read);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, layer3_read);
 
 	/* Checks that previous access rights are unchanged with layer 3. */
 	ASSERT_EQ(0, test_open(file1_s1d3, O_RDWR));
@@ -1276,13 +1237,9 @@ TEST_F_FORK(layout1, interleaved_masked_accesses)
 	ASSERT_EQ(0, test_open(file2_s1d3, O_WRONLY));
 
 	/* This time, denies write access for the file hierarchy. */
-	ruleset_fd = create_ruleset(_metadata,
-				    LANDLOCK_ACCESS_FS_READ_FILE |
-					    LANDLOCK_ACCESS_FS_WRITE_FILE,
-				    layer4_read_write);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE,
+		   layer4_read_write);
 
 	/*
 	 * Checks that the only change with layer 4 is that write access is
@@ -1293,11 +1250,7 @@ TEST_F_FORK(layout1, interleaved_masked_accesses)
 	ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY));
 	ASSERT_EQ(EACCES, test_open(file2_s1d3, O_WRONLY));
 
-	ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_FILE,
-				    layer5_read);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, layer5_read);
 
 	/* Checks that previous access rights are unchanged with layer 5. */
 	ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY));
@@ -1305,11 +1258,7 @@ TEST_F_FORK(layout1, interleaved_masked_accesses)
 	ASSERT_EQ(EACCES, test_open(file2_s1d3, O_WRONLY));
 	ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY));
 
-	ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_EXECUTE,
-				    layer6_execute);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_EXECUTE, layer6_execute);
 
 	/* Checks that previous access rights are unchanged with layer 6. */
 	ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY));
@@ -1317,13 +1266,9 @@ TEST_F_FORK(layout1, interleaved_masked_accesses)
 	ASSERT_EQ(EACCES, test_open(file2_s1d3, O_WRONLY));
 	ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY));
 
-	ruleset_fd = create_ruleset(_metadata,
-				    LANDLOCK_ACCESS_FS_READ_FILE |
-					    LANDLOCK_ACCESS_FS_WRITE_FILE,
-				    layer7_read_write);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE,
+		   layer7_read_write);
 
 	/* Checks read access is now denied with layer 7. */
 	ASSERT_EQ(EACCES, test_open(file1_s1d3, O_RDONLY));
@@ -1344,7 +1289,6 @@ TEST_F_FORK(layout1, inherit_subset)
 	};
 	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
 	enforce_ruleset(_metadata, ruleset_fd);
 
 	ASSERT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY));
@@ -1460,7 +1404,6 @@ TEST_F_FORK(layout1, inherit_superset)
 	};
 	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
 	enforce_ruleset(_metadata, ruleset_fd);
 
 	/* Readdir access is denied for dir_s1d2. */
@@ -1476,7 +1419,7 @@ TEST_F_FORK(layout1, inherit_superset)
 				 LANDLOCK_ACCESS_FS_READ_DIR,
 			 dir_s1d2);
 	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	EXPECT_EQ(0, close(ruleset_fd));
 
 	/* Readdir access is still denied for dir_s1d2. */
 	ASSERT_EQ(EACCES, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY));
@@ -1498,7 +1441,6 @@ TEST_F_FORK(layout0, max_layers)
 	};
 	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
 	for (i = 0; i < 16; i++)
 		enforce_ruleset(_metadata, ruleset_fd);
 
@@ -1507,7 +1449,7 @@ TEST_F_FORK(layout0, max_layers)
 		ASSERT_EQ(-1, err);
 		ASSERT_EQ(E2BIG, errno);
 	}
-	ASSERT_EQ(0, close(ruleset_fd));
+	EXPECT_EQ(0, close(ruleset_fd));
 }
 
 TEST_F_FORK(layout1, empty_or_same_ruleset)
@@ -1521,20 +1463,15 @@ TEST_F_FORK(layout1, empty_or_same_ruleset)
 	ASSERT_LE(-1, ruleset_fd);
 	ASSERT_EQ(ENOMSG, errno);
 
-	/* Enforces policy which deny read access to all files. */
-	ruleset_attr.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE;
-	ruleset_fd =
-		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
+	/* Enforces policy which denies read access to all files. */
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, NULL);
+
 	ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
 	ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY));
 
-	/* Nests a policy which deny read access to all directories. */
-	ruleset_attr.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR;
+	/* Nests a policy which denies read access to all directories. */
 	ruleset_fd =
-		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
-	ASSERT_LE(0, ruleset_fd);
+		create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_DIR, NULL);
 	enforce_ruleset(_metadata, ruleset_fd);
 	ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
 	ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY));
@@ -1558,11 +1495,8 @@ TEST_F_FORK(layout1, rule_on_mountpoint)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY));
 
@@ -1587,11 +1521,8 @@ TEST_F_FORK(layout1, rule_over_mountpoint)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY));
 
@@ -1615,21 +1546,15 @@ TEST_F_FORK(layout1, rule_over_root_allow_then_deny)
 		},
 		{},
 	};
-	int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	/* Checks allowed access. */
 	ASSERT_EQ(0, test_open("/", O_RDONLY));
 	ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY));
 
 	rules[0].access = LANDLOCK_ACCESS_FS_READ_FILE;
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	/* Checks denied access (on a directory). */
 	ASSERT_EQ(EACCES, test_open("/", O_RDONLY));
@@ -1645,11 +1570,8 @@ TEST_F_FORK(layout1, rule_over_root_deny)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	/* Checks denied access (on a directory). */
 	ASSERT_EQ(EACCES, test_open("/", O_RDONLY));
@@ -1665,7 +1587,6 @@ TEST_F_FORK(layout1, rule_inside_mount_ns)
 		},
 		{},
 	};
-	int ruleset_fd;
 
 	set_cap(_metadata, CAP_SYS_ADMIN);
 	ASSERT_EQ(0, syscall(__NR_pivot_root, dir_s3d2, dir_s3d3))
@@ -1675,10 +1596,7 @@ TEST_F_FORK(layout1, rule_inside_mount_ns)
 	ASSERT_EQ(0, chdir("/"));
 	clear_cap(_metadata, CAP_SYS_ADMIN);
 
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	ASSERT_EQ(0, test_open("s3d3", O_RDONLY));
 	ASSERT_EQ(EACCES, test_open("/", O_RDONLY));
@@ -1693,11 +1611,8 @@ TEST_F_FORK(layout1, mount_and_pivot)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	set_cap(_metadata, CAP_SYS_ADMIN);
 	ASSERT_EQ(-1, mount(NULL, dir_s3d2, NULL, MS_RDONLY, NULL));
@@ -1716,9 +1631,6 @@ TEST_F_FORK(layout1, move_mount)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
-
-	ASSERT_LE(0, ruleset_fd);
 
 	set_cap(_metadata, CAP_SYS_ADMIN);
 	ASSERT_EQ(0, syscall(__NR_move_mount, AT_FDCWD, dir_s3d2, AT_FDCWD,
@@ -1731,8 +1643,7 @@ TEST_F_FORK(layout1, move_mount)
 			     dir_s3d2, 0));
 	clear_cap(_metadata, CAP_SYS_ADMIN);
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	set_cap(_metadata, CAP_SYS_ADMIN);
 	ASSERT_EQ(-1, syscall(__NR_move_mount, AT_FDCWD, dir_s3d2, AT_FDCWD,
@@ -1747,14 +1658,9 @@ TEST_F_FORK(layout1, topology_changes_with_net_only)
 		.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
 				      LANDLOCK_ACCESS_NET_CONNECT_TCP,
 	};
-	int ruleset_fd;
 
 	/* Add network restrictions. */
-	ruleset_fd =
-		landlock_create_ruleset(&ruleset_net, sizeof(ruleset_net), 0);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	drop_access_rights(_metadata, &ruleset_net);
 
 	/* Mount, remount, move_mount, umount, and pivot_root checks. */
 	set_cap(_metadata, CAP_SYS_ADMIN);
@@ -1775,14 +1681,9 @@ TEST_F_FORK(layout1, topology_changes_with_net_and_fs)
 				      LANDLOCK_ACCESS_NET_CONNECT_TCP,
 		.handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE,
 	};
-	int ruleset_fd;
 
 	/* Add network and filesystem restrictions. */
-	ruleset_fd = landlock_create_ruleset(&ruleset_net_fs,
-					     sizeof(ruleset_net_fs), 0);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	drop_access_rights(_metadata, &ruleset_net_fs);
 
 	/* Mount, remount, move_mount, umount, and pivot_root checks. */
 	set_cap(_metadata, CAP_SYS_ADMIN);
@@ -1819,14 +1720,13 @@ TEST_F_FORK(layout1, release_inodes)
 	};
 	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
 	/* Unmount a file hierarchy while it is being used by a ruleset. */
 	set_cap(_metadata, CAP_SYS_ADMIN);
 	ASSERT_EQ(0, umount(dir_s3d2));
 	clear_cap(_metadata, CAP_SYS_ADMIN);
 
 	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	EXPECT_EQ(0, close(ruleset_fd));
 
 	ASSERT_EQ(0, test_open(file1_s1d1, O_RDONLY));
 	ASSERT_EQ(EACCES, test_open(dir_s3d2, O_RDONLY));
@@ -1858,7 +1758,6 @@ TEST_F_FORK(layout1, covered_rule)
 	/* Creates a ruleset with the future hidden directory. */
 	ruleset_fd =
 		create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_DIR, layer1);
-	ASSERT_LE(0, ruleset_fd);
 
 	/* Covers with a new mount point. */
 	set_cap(_metadata, CAP_SYS_ADMIN);
@@ -1908,10 +1807,7 @@ static void test_relative_path(struct __test_metadata *const _metadata,
 	};
 	int dirfd, ruleset_fd;
 
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer1_base);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer1_base);
 
 	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer2_subs);
 
@@ -2092,10 +1988,7 @@ TEST_F_FORK(layout1, execute)
 		},
 		{},
 	};
-	const int ruleset_fd =
-		create_ruleset(_metadata, rules[0].access, rules);
 
-	ASSERT_LE(0, ruleset_fd);
 	copy_file(_metadata, bin_true, file1_s1d1);
 	copy_file(_metadata, bin_true, file1_s1d2);
 	copy_file(_metadata, bin_true, file1_s1d3);
@@ -2104,8 +1997,7 @@ TEST_F_FORK(layout1, execute)
 	test_execute(_metadata, 0, file1_s1d1);
 	test_check_exec(_metadata, 0, file1_s1d1);
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, rules[0].access, rules);
 
 	ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY));
 	ASSERT_EQ(0, test_open(file1_s1d1, O_RDONLY));
@@ -2216,16 +2108,12 @@ TEST_F_FORK(layout1, link)
 		},
 		{},
 	};
-	int ruleset_fd = create_ruleset(_metadata, layer1[0].access, layer1);
-
-	ASSERT_LE(0, ruleset_fd);
 
 	ASSERT_EQ(0, unlink(file1_s1d1));
 	ASSERT_EQ(0, unlink(file1_s1d2));
 	ASSERT_EQ(0, unlink(file1_s1d3));
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, layer1[0].access, layer1);
 
 	ASSERT_EQ(-1, link(file2_s1d1, file1_s1d1));
 	ASSERT_EQ(EACCES, errno);
@@ -2245,10 +2133,7 @@ TEST_F_FORK(layout1, link)
 	ASSERT_EQ(0, unlink(file2_s1d2));
 	ASSERT_EQ(0, unlink(file2_s1d3));
 
-	ruleset_fd = create_ruleset(_metadata, layer2[0].access, layer2);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, layer2[0].access, layer2);
 
 	/* Checks that linkind doesn't require the ability to delete a file. */
 	ASSERT_EQ(0, link(file1_s1d2, file2_s1d2));
@@ -2298,15 +2183,10 @@ TEST_F_FORK(layout1, rename_file)
 		},
 		{},
 	};
-	const int ruleset_fd =
-		create_ruleset(_metadata, rules[0].access, rules);
-
-	ASSERT_LE(0, ruleset_fd);
 
 	ASSERT_EQ(0, unlink(file1_s1d2));
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, rules[0].access, rules);
 
 	/*
 	 * Tries to replace a file, from a directory that allows file removal,
@@ -2380,17 +2260,12 @@ TEST_F_FORK(layout1, rename_dir)
 		},
 		{},
 	};
-	const int ruleset_fd =
-		create_ruleset(_metadata, rules[0].access, rules);
-
-	ASSERT_LE(0, ruleset_fd);
 
 	/* Empties dir_s1d3 to allow renaming. */
 	ASSERT_EQ(0, unlink(file1_s1d3));
 	ASSERT_EQ(0, unlink(file2_s1d3));
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, rules[0].access, rules);
 
 	/* Exchanges and renames directory to a different parent. */
 	ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_s2d3, AT_FDCWD, dir_s1d3,
@@ -2444,12 +2319,8 @@ TEST_F_FORK(layout1, reparent_refer)
 		},
 		{},
 	};
-	int ruleset_fd =
-		create_ruleset(_metadata, LANDLOCK_ACCESS_FS_REFER, layer1);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_REFER, layer1);
 
 	ASSERT_EQ(-1, rename(dir_s1d2, dir_s2d1));
 	ASSERT_EQ(EXDEV, errno);
@@ -2479,14 +2350,9 @@ static void refer_denied_by_default(struct __test_metadata *const _metadata,
 				    const int layer1_err,
 				    const struct rule layer2[])
 {
-	int ruleset_fd;
-
 	ASSERT_EQ(0, unlink(file1_s1d2));
 
-	ruleset_fd = create_ruleset(_metadata, layer1[0].access, layer1);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, layer1[0].access, layer1);
 
 	/*
 	 * If the first layer handles LANDLOCK_ACCESS_FS_REFER (according to
@@ -2498,10 +2364,7 @@ static void refer_denied_by_default(struct __test_metadata *const _metadata,
 	ASSERT_EQ(layer1_err, test_exchange(file2_s1d1, file2_s1d2));
 	ASSERT_EQ(layer1_err, test_exchange(file2_s1d2, file2_s1d1));
 
-	ruleset_fd = create_ruleset(_metadata, layer2[0].access, layer2);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, layer2[0].access, layer2);
 
 	/*
 	 * Now, either the first or the second layer does not handle
@@ -2587,10 +2450,7 @@ TEST_F_FORK(layout1, refer_denied_by_default4)
  */
 TEST_F_FORK(layout1, refer_mount_root_deny)
 {
-	const struct landlock_ruleset_attr ruleset_attr = {
-		.handled_access_fs = LANDLOCK_ACCESS_FS_MAKE_DIR,
-	};
-	int root_fd, ruleset_fd;
+	int root_fd;
 
 	/* Creates a mount object from a non-mount point. */
 	set_cap(_metadata, CAP_SYS_ADMIN);
@@ -2600,13 +2460,7 @@ TEST_F_FORK(layout1, refer_mount_root_deny)
 	clear_cap(_metadata, CAP_SYS_ADMIN);
 	ASSERT_LE(0, root_fd);
 
-	ruleset_fd =
-		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
-	ASSERT_LE(0, ruleset_fd);
-
-	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
-	ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
-	EXPECT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_DIR, NULL);
 
 	/* Link denied by Landlock: EACCES. */
 	EXPECT_EQ(-1, linkat(root_fd, ".", root_fd, "does_not_exist", 0));
@@ -2641,18 +2495,12 @@ TEST_F_FORK(layout1, refer_part_mount_tree_is_allowed)
 		},
 		{},
 	};
-	int ruleset_fd;
 
 	ASSERT_EQ(0, unlink(file1_s3d3));
-	ruleset_fd = create_ruleset(_metadata,
-				    LANDLOCK_ACCESS_FS_REFER |
-					    LANDLOCK_ACCESS_FS_MAKE_REG |
-					    LANDLOCK_ACCESS_FS_REMOVE_FILE,
-				    layer1);
-
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG |
+			   LANDLOCK_ACCESS_FS_REMOVE_FILE,
+		   layer1);
 
 	ASSERT_EQ(0, rename(file1_s3d4, file1_s3d3));
 }
@@ -2678,13 +2526,10 @@ TEST_F_FORK(layout1, reparent_link)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(
-		_metadata,
-		LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER, layer1);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER,
+		   layer1);
 
 	ASSERT_EQ(0, unlink(file1_s1d1));
 	ASSERT_EQ(0, unlink(file1_s1d2));
@@ -2756,13 +2601,10 @@ TEST_F_FORK(layout1, reparent_rename)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(
-		_metadata,
-		LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER, layer1);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER,
+		   layer1);
 
 	ASSERT_EQ(0, unlink(file1_s1d2));
 	ASSERT_EQ(0, unlink(file1_s1d3));
@@ -2902,13 +2744,9 @@ reparent_exdev_layers_enforce1(struct __test_metadata *const _metadata)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(
-		_metadata,
-		LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER, layer1);
-
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER,
+		   layer1);
 }
 
 static void
@@ -2925,12 +2763,7 @@ reparent_exdev_layers_enforce2(struct __test_metadata *const _metadata)
 	 * Same checks as before but with a second layer and a new MAKE_DIR
 	 * rule (and no explicit handling of REFER).
 	 */
-	const int ruleset_fd =
-		create_ruleset(_metadata, LANDLOCK_ACCESS_FS_MAKE_DIR, layer2);
-
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_DIR, layer2);
 }
 
 TEST_F_FORK(layout1, reparent_exdev_layers_rename1)
@@ -3199,15 +3032,11 @@ TEST_F_FORK(layout1, reparent_remove)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(
-		_metadata,
-		LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_REMOVE_DIR |
-			LANDLOCK_ACCESS_FS_REMOVE_FILE,
-		layer1);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_REMOVE_DIR |
+			   LANDLOCK_ACCESS_FS_REMOVE_FILE,
+		   layer1);
 
 	/* Access denied because of wrong/swapped remove file/dir. */
 	ASSERT_EQ(-1, rename(file1_s1d1, dir_s2d2));
@@ -3271,17 +3100,13 @@ TEST_F_FORK(layout1, reparent_dom_superset)
 		},
 		{},
 	};
-	int ruleset_fd = create_ruleset(_metadata,
-					LANDLOCK_ACCESS_FS_REFER |
-						LANDLOCK_ACCESS_FS_EXECUTE |
-						LANDLOCK_ACCESS_FS_MAKE_SOCK |
-						LANDLOCK_ACCESS_FS_READ_FILE |
-						LANDLOCK_ACCESS_FS_MAKE_FIFO,
-					layer1);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_EXECUTE |
+			   LANDLOCK_ACCESS_FS_MAKE_SOCK |
+			   LANDLOCK_ACCESS_FS_READ_FILE |
+			   LANDLOCK_ACCESS_FS_MAKE_FIFO,
+		   layer1);
 
 	ASSERT_EQ(-1, rename(file1_s1d2, file1_s2d1));
 	ASSERT_EQ(EXDEV, errno);
@@ -3344,18 +3169,13 @@ TEST_F_FORK(layout1, remove_dir)
 		},
 		{},
 	};
-	const int ruleset_fd =
-		create_ruleset(_metadata, rules[0].access, rules);
-
-	ASSERT_LE(0, ruleset_fd);
 
 	ASSERT_EQ(0, unlink(file1_s1d1));
 	ASSERT_EQ(0, unlink(file1_s1d2));
 	ASSERT_EQ(0, unlink(file1_s1d3));
 	ASSERT_EQ(0, unlink(file2_s1d3));
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, rules[0].access, rules);
 
 	ASSERT_EQ(0, rmdir(dir_s1d3));
 	ASSERT_EQ(0, mkdir(dir_s1d3, 0700));
@@ -3381,12 +3201,8 @@ TEST_F_FORK(layout1, remove_file)
 		},
 		{},
 	};
-	const int ruleset_fd =
-		create_ruleset(_metadata, rules[0].access, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, rules[0].access, rules);
 
 	ASSERT_EQ(-1, unlink(file1_s1d1));
 	ASSERT_EQ(EACCES, errno);
@@ -3407,9 +3223,6 @@ static void test_make_file(struct __test_metadata *const _metadata,
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(_metadata, access, rules);
-
-	ASSERT_LE(0, ruleset_fd);
 
 	ASSERT_EQ(0, unlink(file1_s1d1));
 	ASSERT_EQ(0, unlink(file2_s1d1));
@@ -3425,8 +3238,7 @@ static void test_make_file(struct __test_metadata *const _metadata,
 	ASSERT_EQ(0, unlink(file1_s1d3));
 	ASSERT_EQ(0, unlink(file2_s1d3));
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, access, rules);
 
 	ASSERT_EQ(-1, mknod(file1_s1d1, mode | 0400, dev));
 	ASSERT_EQ(EACCES, errno);
@@ -3495,10 +3307,6 @@ TEST_F_FORK(layout1, make_sym)
 		},
 		{},
 	};
-	const int ruleset_fd =
-		create_ruleset(_metadata, rules[0].access, rules);
-
-	ASSERT_LE(0, ruleset_fd);
 
 	ASSERT_EQ(0, unlink(file1_s1d1));
 	ASSERT_EQ(0, unlink(file2_s1d1));
@@ -3510,8 +3318,7 @@ TEST_F_FORK(layout1, make_sym)
 	ASSERT_EQ(0, unlink(file1_s1d3));
 	ASSERT_EQ(0, unlink(file2_s1d3));
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, rules[0].access, rules);
 
 	ASSERT_EQ(-1, symlink("none", file1_s1d1));
 	ASSERT_EQ(EACCES, errno);
@@ -3540,17 +3347,12 @@ TEST_F_FORK(layout1, make_dir)
 		},
 		{},
 	};
-	const int ruleset_fd =
-		create_ruleset(_metadata, rules[0].access, rules);
-
-	ASSERT_LE(0, ruleset_fd);
 
 	ASSERT_EQ(0, unlink(file1_s1d1));
 	ASSERT_EQ(0, unlink(file1_s1d2));
 	ASSERT_EQ(0, unlink(file1_s1d3));
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, rules[0].access, rules);
 
 	/* Uses file_* as directory names. */
 	ASSERT_EQ(-1, mkdir(file1_s1d1, 0700));
@@ -3581,14 +3383,10 @@ TEST_F_FORK(layout1, proc_unlinked_file)
 		{},
 	};
 	int reg_fd, proc_fd;
-	const int ruleset_fd = create_ruleset(
-		_metadata,
-		LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE,
-		rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE,
+		   rules);
 
 	ASSERT_EQ(EACCES, test_open(file1_s1d2, O_RDWR));
 	ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY));
@@ -3624,13 +3422,9 @@ TEST_F_FORK(layout1, proc_pipe)
 		},
 		{},
 	};
-	/* Limits read and write access to files tied to the filesystem. */
-	const int ruleset_fd =
-		create_ruleset(_metadata, rules[0].access, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	/* Limits read and write access to files tied to the filesystem. */
+	enforce_fs(_metadata, rules[0].access, rules);
 
 	/* Checks enforcement for normal files. */
 	ASSERT_EQ(0, test_open(file1_s1d2, O_RDWR));
@@ -3720,16 +3514,10 @@ TEST_F_FORK(layout1, truncate_unhandled)
 		{},
 	};
 
-	const __u64 handled = LANDLOCK_ACCESS_FS_READ_FILE |
-			      LANDLOCK_ACCESS_FS_WRITE_FILE;
-	int ruleset_fd;
-
 	/* Enables Landlock. */
-	ruleset_fd = create_ruleset(_metadata, handled, rules);
-
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE,
+		   rules);
 
 	/*
 	 * Checks read right: truncate and open with O_TRUNC work, unless the
@@ -3802,17 +3590,13 @@ TEST_F_FORK(layout1, truncate)
 		},
 		{},
 	};
-	const __u64 handled = LANDLOCK_ACCESS_FS_READ_FILE |
-			      LANDLOCK_ACCESS_FS_WRITE_FILE |
-			      LANDLOCK_ACCESS_FS_TRUNCATE;
-	int ruleset_fd;
 
 	/* Enables Landlock. */
-	ruleset_fd = create_ruleset(_metadata, handled, rules);
-
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_READ_FILE |
+			   LANDLOCK_ACCESS_FS_WRITE_FILE |
+			   LANDLOCK_ACCESS_FS_TRUNCATE,
+		   rules);
 
 	/* Checks read, write and truncate rights: truncation works. */
 	EXPECT_EQ(0, test_truncate(file_rwt));
@@ -3912,34 +3696,25 @@ TEST_F_FORK(layout1, ftruncate)
 		},
 		{},
 	};
-	int fd_layer0, fd_layer1, fd_layer2, fd_layer3, ruleset_fd;
+	int fd_layer0, fd_layer1, fd_layer2, fd_layer3;
 
 	fd_layer0 = open(path, O_WRONLY);
 	EXPECT_EQ(0, test_ftruncate(fd_layer0));
 
-	ruleset_fd = create_ruleset(_metadata, handled1, layer1);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, handled1, layer1);
 
 	fd_layer1 = open(path, O_WRONLY);
 	EXPECT_EQ(0, test_ftruncate(fd_layer0));
 	EXPECT_EQ(0, test_ftruncate(fd_layer1));
 
-	ruleset_fd = create_ruleset(_metadata, handled2, layer2);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, handled2, layer2);
 
 	fd_layer2 = open(path, O_WRONLY);
 	EXPECT_EQ(0, test_ftruncate(fd_layer0));
 	EXPECT_EQ(0, test_ftruncate(fd_layer1));
 	EXPECT_EQ(0, test_ftruncate(fd_layer2));
 
-	ruleset_fd = create_ruleset(_metadata, handled3, layer3);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, handled3, layer3);
 
 	fd_layer3 = open(path, O_WRONLY);
 	EXPECT_EQ(0, test_ftruncate(fd_layer0));
@@ -4031,13 +3806,10 @@ TEST_F_FORK(ftruncate, open_and_ftruncate)
 		},
 		{},
 	};
-	int fd, ruleset_fd;
+	int fd;
 
 	/* Enables Landlock. */
-	ruleset_fd = create_ruleset(_metadata, variant->handled, rules);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, variant->handled, rules);
 
 	fd = open(path, O_WRONLY);
 	EXPECT_EQ(variant->expected_open_result, (fd < 0 ? errno : 0));
@@ -4072,12 +3844,9 @@ TEST_F_FORK(ftruncate, open_and_ftruncate_in_different_processes)
 			},
 			{},
 		};
-		int fd, ruleset_fd;
+		int fd;
 
-		ruleset_fd = create_ruleset(_metadata, variant->handled, rules);
-		ASSERT_LE(0, ruleset_fd);
-		enforce_ruleset(_metadata, ruleset_fd);
-		ASSERT_EQ(0, close(ruleset_fd));
+		enforce_fs(_metadata, variant->handled, rules);
 
 		fd = open(path, O_WRONLY);
 		ASSERT_EQ(variant->expected_open_result, (fd < 0 ? errno : 0));
@@ -4122,10 +3891,7 @@ static int test_fs_ioc_getflags_ioctl(int fd)
 
 TEST(memfd_ftruncate_and_ioctl)
 {
-	const struct landlock_ruleset_attr attr = {
-		.handled_access_fs = ACCESS_ALL,
-	};
-	int ruleset_fd, fd, i;
+	int fd, i;
 
 	/*
 	 * We exercise the same test both with and without Landlock enabled, to
@@ -4147,10 +3913,7 @@ TEST(memfd_ftruncate_and_ioctl)
 		ASSERT_EQ(0, close(fd));
 
 		/* Enables Landlock. */
-		ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
-		ASSERT_LE(0, ruleset_fd);
-		enforce_ruleset(_metadata, ruleset_fd);
-		ASSERT_EQ(0, close(ruleset_fd));
+		enforce_fs(_metadata, ACCESS_ALL, NULL);
 	}
 }
 
@@ -4165,10 +3928,7 @@ static int test_fionread_ioctl(int fd)
 
 TEST_F_FORK(layout1, o_path_ftruncate_and_ioctl)
 {
-	const struct landlock_ruleset_attr attr = {
-		.handled_access_fs = ACCESS_ALL,
-	};
-	int ruleset_fd, fd;
+	int fd;
 
 	/*
 	 * Checks that for files opened with O_PATH, both ioctl(2) and
@@ -4184,10 +3944,7 @@ TEST_F_FORK(layout1, o_path_ftruncate_and_ioctl)
 	ASSERT_EQ(0, close(fd));
 
 	/* Enables Landlock. */
-	ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	/*
 	 * Checks that after enabling Landlock,
@@ -4261,16 +4018,10 @@ struct space_resv {
  */
 TEST_F_FORK(layout1, blanket_permitted_ioctls)
 {
-	const struct landlock_ruleset_attr attr = {
-		.handled_access_fs = LANDLOCK_ACCESS_FS_IOCTL_DEV,
-	};
-	int ruleset_fd, fd;
+	int fd;
 
 	/* Enables Landlock. */
-	ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_IOCTL_DEV, NULL);
 
 	fd = open("/dev/null", O_RDWR | O_CLOEXEC);
 	ASSERT_LE(0, fd);
@@ -4323,20 +4074,14 @@ TEST_F_FORK(layout1, blanket_permitted_ioctls)
 TEST_F_FORK(layout1, named_pipe_ioctl)
 {
 	pid_t child_pid;
-	int fd, ruleset_fd;
+	int fd;
 	const char *const path = file1_s1d1;
-	const struct landlock_ruleset_attr attr = {
-		.handled_access_fs = LANDLOCK_ACCESS_FS_IOCTL_DEV,
-	};
 
 	ASSERT_EQ(0, unlink(path));
 	ASSERT_EQ(0, mkfifo(path, 0600));
 
 	/* Enables Landlock. */
-	ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_IOCTL_DEV, NULL);
 
 	/* The child process opens the pipe for writing. */
 	child_pid = fork();
@@ -4406,20 +4151,14 @@ static int test_connect_named_unix(int fd, const char *const path)
 TEST_F_FORK(layout1, named_unix_domain_socket_ioctl)
 {
 	const char *const path = file1_s1d1;
-	int srv_fd, cli_fd, ruleset_fd;
-	const struct landlock_ruleset_attr attr = {
-		.handled_access_fs = LANDLOCK_ACCESS_FS_IOCTL_DEV,
-	};
+	int srv_fd, cli_fd;
 
 	/* Sets up a server */
 	ASSERT_EQ(0, unlink(path));
 	srv_fd = set_up_named_unix_server(_metadata, SOCK_STREAM, path);
 
 	/* Enables Landlock. */
-	ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_IOCTL_DEV, NULL);
 
 	/* Sets up a client connection to it */
 	cli_fd = socket(AF_UNIX, SOCK_STREAM, 0);
@@ -4492,29 +4231,25 @@ TEST_F_FORK(ioctl, handle_dir_access_file)
 		},
 		{},
 	};
-	int file_fd, ruleset_fd;
+	int fd;
 
 	/* Enables Landlock. */
-	ruleset_fd = create_ruleset(_metadata, variant->handled, rules);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, variant->handled, rules);
 
-	file_fd = open("/dev/zero", variant->open_mode);
-	ASSERT_LE(0, file_fd);
+	fd = open("/dev/zero", variant->open_mode);
+	ASSERT_LE(0, fd);
 
 	/* Checks that IOCTL commands return the expected errors. */
-	EXPECT_EQ(variant->expected_fionread_result,
-		  test_fionread_ioctl(file_fd));
+	EXPECT_EQ(variant->expected_fionread_result, test_fionread_ioctl(fd));
 
 	/* Checks that unrestrictable commands are unrestricted. */
-	EXPECT_EQ(0, ioctl(file_fd, FIOCLEX));
-	EXPECT_EQ(0, ioctl(file_fd, FIONCLEX));
-	EXPECT_EQ(0, ioctl(file_fd, FIONBIO, &flag));
-	EXPECT_EQ(0, ioctl(file_fd, FIOASYNC, &flag));
-	EXPECT_EQ(0, ioctl(file_fd, FIGETBSZ, &flag));
+	EXPECT_EQ(0, ioctl(fd, FIOCLEX));
+	EXPECT_EQ(0, ioctl(fd, FIONCLEX));
+	EXPECT_EQ(0, ioctl(fd, FIONBIO, &flag));
+	EXPECT_EQ(0, ioctl(fd, FIOASYNC, &flag));
+	EXPECT_EQ(0, ioctl(fd, FIGETBSZ, &flag));
 
-	ASSERT_EQ(0, close(file_fd));
+	ASSERT_EQ(0, close(fd));
 }
 
 TEST_F_FORK(ioctl, handle_dir_access_dir)
@@ -4527,13 +4262,10 @@ TEST_F_FORK(ioctl, handle_dir_access_dir)
 		},
 		{},
 	};
-	int dir_fd, ruleset_fd;
+	int dir_fd;
 
 	/* Enables Landlock. */
-	ruleset_fd = create_ruleset(_metadata, variant->handled, rules);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, variant->handled, rules);
 
 	/*
 	 * Ignore variant->open_mode for this test, as we intend to open a
@@ -4572,32 +4304,28 @@ TEST_F_FORK(ioctl, handle_file_access_file)
 		},
 		{},
 	};
-	int file_fd, ruleset_fd;
+	int fd;
 
 	/* Enables Landlock. */
-	ruleset_fd = create_ruleset(_metadata, variant->handled, rules);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, variant->handled, rules);
 
-	file_fd = open("/dev/zero", variant->open_mode);
-	ASSERT_LE(0, file_fd)
+	fd = open("/dev/zero", variant->open_mode);
+	ASSERT_LE(0, fd)
 	{
 		TH_LOG("Failed to open /dev/zero: %s", strerror(errno));
 	}
 
 	/* Checks that IOCTL commands return the expected errors. */
-	EXPECT_EQ(variant->expected_fionread_result,
-		  test_fionread_ioctl(file_fd));
+	EXPECT_EQ(variant->expected_fionread_result, test_fionread_ioctl(fd));
 
 	/* Checks that unrestrictable commands are unrestricted. */
-	EXPECT_EQ(0, ioctl(file_fd, FIOCLEX));
-	EXPECT_EQ(0, ioctl(file_fd, FIONCLEX));
-	EXPECT_EQ(0, ioctl(file_fd, FIONBIO, &flag));
-	EXPECT_EQ(0, ioctl(file_fd, FIOASYNC, &flag));
-	EXPECT_EQ(0, ioctl(file_fd, FIGETBSZ, &flag));
+	EXPECT_EQ(0, ioctl(fd, FIOCLEX));
+	EXPECT_EQ(0, ioctl(fd, FIONCLEX));
+	EXPECT_EQ(0, ioctl(fd, FIONBIO, &flag));
+	EXPECT_EQ(0, ioctl(fd, FIOASYNC, &flag));
+	EXPECT_EQ(0, ioctl(fd, FIGETBSZ, &flag));
 
-	ASSERT_EQ(0, close(file_fd));
+	ASSERT_EQ(0, close(fd));
 }
 
 /*
@@ -4636,24 +4364,6 @@ FIXTURE_TEARDOWN(scoped_domains)
 {
 }
 
-static void enforce_fs_resolve_unix(struct __test_metadata *const _metadata,
-				    const struct rule rules[])
-{
-	if (rules) {
-		int fd = create_ruleset(_metadata,
-					LANDLOCK_ACCESS_FS_RESOLVE_UNIX, rules);
-		enforce_ruleset(_metadata, fd);
-		EXPECT_EQ(0, close(fd));
-	} else {
-		drop_access_rights(
-			_metadata,
-			&(struct landlock_ruleset_attr){
-				.handled_access_fs =
-					LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
-			});
-	}
-}
-
 /*
  * Flags for test_connect_to_parent and test_connect_to_child:
  *
@@ -4684,9 +4394,9 @@ static void test_connect_to_parent(struct __test_metadata *const _metadata,
 	char buf[1];
 
 	if (variant->domain_both)
-		enforce_fs_resolve_unix(_metadata, NULL);
+		enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, NULL);
 	else if (flags & ENFORCE_ALL)
-		enforce_fs_resolve_unix(_metadata, rules);
+		enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, rules);
 
 	unlink(path);
 	ASSERT_EQ(0, pipe2(readiness_pipe, O_CLOEXEC));
@@ -4696,9 +4406,11 @@ static void test_connect_to_parent(struct __test_metadata *const _metadata,
 
 	if (child_pid == 0) {
 		if (variant->domain_child)
-			enforce_fs_resolve_unix(_metadata, NULL);
+			enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+				   NULL);
 		else if (flags & ENFORCE_ALL)
-			enforce_fs_resolve_unix(_metadata, rules);
+			enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+				   rules);
 
 		/* Wait for server to be available. */
 		EXPECT_EQ(0, close(readiness_pipe[1]));
@@ -4724,9 +4436,9 @@ static void test_connect_to_parent(struct __test_metadata *const _metadata,
 	}
 
 	if (variant->domain_parent)
-		enforce_fs_resolve_unix(_metadata, NULL);
+		enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, NULL);
 	else if (flags & ENFORCE_ALL)
-		enforce_fs_resolve_unix(_metadata, rules);
+		enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, rules);
 
 	srv_fd = set_up_named_unix_server(_metadata, sock_type, path);
 
@@ -4765,9 +4477,9 @@ static void test_connect_to_child(struct __test_metadata *const _metadata,
 	char buf[1];
 
 	if (variant->domain_both)
-		enforce_fs_resolve_unix(_metadata, NULL);
+		enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, NULL);
 	else if (flags & ENFORCE_ALL)
-		enforce_fs_resolve_unix(_metadata, rules);
+		enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, rules);
 
 	unlink(path);
 	ASSERT_EQ(0, pipe2(readiness_pipe, O_CLOEXEC));
@@ -4778,9 +4490,11 @@ static void test_connect_to_child(struct __test_metadata *const _metadata,
 
 	if (child_pid == 0) {
 		if (variant->domain_child)
-			enforce_fs_resolve_unix(_metadata, NULL);
+			enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+				   NULL);
 		else if (flags & ENFORCE_ALL)
-			enforce_fs_resolve_unix(_metadata, rules);
+			enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+				   rules);
 
 		srv_fd = set_up_named_unix_server(_metadata, sock_type, path);
 
@@ -4804,9 +4518,9 @@ static void test_connect_to_child(struct __test_metadata *const _metadata,
 	}
 
 	if (variant->domain_parent)
-		enforce_fs_resolve_unix(_metadata, NULL);
+		enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, NULL);
 	else if (flags & ENFORCE_ALL)
-		enforce_fs_resolve_unix(_metadata, rules);
+		enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, rules);
 
 	/* Wait for server to be available. */
 	EXPECT_EQ(0, close(readiness_pipe[1]));
@@ -4996,9 +4710,7 @@ TEST_F_FORK(coredump, socket_not_restricted)
 	clear_cap(_metadata, CAP_SYS_ADMIN);
 
 	/* Restrict LANDLOCK_ACCESS_FS_RESOLVE_UNIX. */
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-		.handled_access_fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
-	});
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, NULL);
 
 	/* Fork a child that crashes. */
 	child_pid = fork();
@@ -5185,13 +4897,9 @@ TEST_F_FORK(layout1_bind, same_content_same_file)
 		},
 		{},
 	};
-	int ruleset_fd;
 
 	/* Sets rules for the parent directories. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer1_parent);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer1_parent);
 
 	/* Checks source hierarchy. */
 	ASSERT_EQ(0, test_open(file1_s1d1, O_RDONLY));
@@ -5210,10 +4918,7 @@ TEST_F_FORK(layout1_bind, same_content_same_file)
 	ASSERT_EQ(0, test_open(dir_s2d2, O_RDONLY | O_DIRECTORY));
 
 	/* Sets rules for the mount points. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer2_mount_point);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer2_mount_point);
 
 	/* Checks source hierarchy. */
 	ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
@@ -5234,10 +4939,7 @@ TEST_F_FORK(layout1_bind, same_content_same_file)
 	ASSERT_EQ(0, test_open(bind_dir_s1d3, O_RDONLY | O_DIRECTORY));
 
 	/* Sets a (shared) rule only on the source. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer3_source);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer3_source);
 
 	/* Checks source hierarchy. */
 	ASSERT_EQ(EACCES, test_open(file1_s1d2, O_RDONLY));
@@ -5258,10 +4960,7 @@ TEST_F_FORK(layout1_bind, same_content_same_file)
 	ASSERT_EQ(EACCES, test_open(bind_dir_s1d3, O_RDONLY | O_DIRECTORY));
 
 	/* Sets a (shared) rule only on the destination. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer4_destination);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer4_destination);
 
 	/* Checks source hierarchy. */
 	ASSERT_EQ(EACCES, test_open(file1_s1d3, O_RDONLY));
@@ -5286,13 +4985,10 @@ TEST_F_FORK(layout1_bind, reparent_cross_mount)
 		},
 		{},
 	};
-	int ruleset_fd = create_ruleset(
-		_metadata,
-		LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_EXECUTE, layer1);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_EXECUTE,
+		   layer1);
 
 	/* Checks basic denied move. */
 	ASSERT_EQ(-1, rename(file1_s1d1, file1_s1d2));
@@ -5349,10 +5045,6 @@ TEST_F_FORK(layout1_bind, path_disconnected)
 		create_ruleset(_metadata, ACCESS_RW, layer3_only_s1d2);
 	int bind_s1d3_fd;
 
-	ASSERT_LE(0, ruleset_fd_l1);
-	ASSERT_LE(0, ruleset_fd_l2);
-	ASSERT_LE(0, ruleset_fd_l3);
-
 	enforce_ruleset(_metadata, ruleset_fd_l1);
 	EXPECT_EQ(0, close(ruleset_fd_l1));
 
@@ -5456,8 +5148,6 @@ TEST_F_FORK(layout1_bind, path_disconnected_rename)
 	ruleset_fd_l1 = create_ruleset(_metadata, ACCESS_ALL, layer1);
 	ruleset_fd_l2 = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_FILE,
 				       layer2_only_s1d2);
-	ASSERT_LE(0, ruleset_fd_l1);
-	ASSERT_LE(0, ruleset_fd_l2);
 
 	enforce_ruleset(_metadata, ruleset_fd_l1);
 	EXPECT_EQ(0, close(ruleset_fd_l1));
@@ -5603,7 +5293,7 @@ TEST_F_FORK(layout1_bind, path_disconnected_link)
 		},
 		{}
 	};
-	int ruleset_fd, bind_s1d3_fd;
+	int bind_s1d3_fd;
 
 	/* Removes unneeded files created by layout1, otherwise it will EEXIST. */
 	ASSERT_EQ(0, unlink(file1_s1d2));
@@ -5626,10 +5316,7 @@ TEST_F_FORK(layout1_bind, path_disconnected_link)
 		TH_LOG("Failed to create %s: %s", dir_s4d2, strerror(errno));
 	}
 
-	ruleset_fd = create_ruleset(_metadata, ACCESS_ALL, layer1);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	EXPECT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_ALL, layer1);
 
 	/* From disconnected to connected. */
 	ASSERT_EQ(0, linkat(bind_s1d3_fd, file1_name, AT_FDCWD, file1_s2d2, 0))
@@ -6167,7 +5854,6 @@ TEST_F_FORK(layout4_disconnected_leafs, read_rename_exchange)
 	int ruleset_fd, s1d41_bind_fd, s1d42_bind_fd;
 
 	ruleset_fd = create_ruleset(_metadata, handled_access, rules);
-	ASSERT_LE(0, ruleset_fd);
 
 	/* Adds rule for the covered directory. */
 	if (variant->allowed_s2d2) {
@@ -7100,7 +6786,6 @@ TEST_F_FORK(layout2_overlay, same_content_different_file)
 		},
 		{},
 	};
-	int ruleset_fd;
 	size_t i;
 	const char *path_entry;
 
@@ -7108,10 +6793,7 @@ TEST_F_FORK(layout2_overlay, same_content_different_file)
 		SKIP(return, "overlayfs is not supported (test)");
 
 	/* Sets rules on base directories (i.e. outside overlay scope). */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer1_base);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer1_base);
 
 	/* Checks lower layer. */
 	for_each_path(lower_base_files, path_entry, i) {
@@ -7156,10 +6838,7 @@ TEST_F_FORK(layout2_overlay, same_content_different_file)
 	}
 
 	/* Sets rules on data directories (i.e. inside overlay scope). */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer2_data);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer2_data);
 
 	/* Checks merge. */
 	for_each_path(merge_base_files, path_entry, i) {
@@ -7173,10 +6852,7 @@ TEST_F_FORK(layout2_overlay, same_content_different_file)
 	}
 
 	/* Same checks with tighter rules. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer3_subdirs);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer3_subdirs);
 
 	/* Checks changes for lower layer. */
 	for_each_path(lower_base_files, path_entry, i) {
@@ -7198,10 +6874,7 @@ TEST_F_FORK(layout2_overlay, same_content_different_file)
 	}
 
 	/* Sets rules directly on overlayed files. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer4_files);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer4_files);
 
 	/* Checks unchanged accesses on lower layer. */
 	for_each_path(lower_sub_files, path_entry, i) {
@@ -7226,10 +6899,7 @@ TEST_F_FORK(layout2_overlay, same_content_different_file)
 	}
 
 	/* Only allowes access to the merge hierarchy. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer5_merge_only);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer5_merge_only);
 
 	/* Checks new accesses on lower layer. */
 	for_each_path(lower_sub_files, path_entry, i) {
@@ -7415,11 +7085,7 @@ static void layer3_fs_tag_inode(struct __test_metadata *const _metadata,
 		},
 		{},
 	};
-	const struct landlock_ruleset_attr layer2_deny_everything_attr = {
-		.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
-	};
 	const char *const dev_null_path = "/dev/null";
-	int ruleset_fd;
 
 	if (self->skip_test)
 		SKIP(return, "this filesystem is not supported (test)");
@@ -7428,22 +7094,14 @@ static void layer3_fs_tag_inode(struct __test_metadata *const _metadata,
 	EXPECT_EQ(0, test_open(dev_null_path, O_RDONLY | O_CLOEXEC));
 	EXPECT_EQ(0, test_open(variant->file_path, O_RDONLY | O_CLOEXEC));
 
-	ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_FILE,
-				    layer1_allow_read_file);
-	EXPECT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	EXPECT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE,
+		   layer1_allow_read_file);
 
 	EXPECT_EQ(EACCES, test_open(dev_null_path, O_RDONLY | O_CLOEXEC));
 	EXPECT_EQ(0, test_open(variant->file_path, O_RDONLY | O_CLOEXEC));
 
 	/* Forbids directory reading. */
-	ruleset_fd =
-		landlock_create_ruleset(&layer2_deny_everything_attr,
-					sizeof(layer2_deny_everything_attr), 0);
-	EXPECT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	EXPECT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, NULL);
 
 	/* Checks with Landlock and forbidden access. */
 	EXPECT_EQ(EACCES, test_open(dev_null_path, O_RDONLY | O_CLOEXEC));
@@ -7505,7 +7163,6 @@ TEST_F_FORK(layout3_fs, release_inodes)
 
 	ruleset_fd =
 		create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_DIR, layer1);
-	ASSERT_LE(0, ruleset_fd);
 
 	/* Unmount the filesystem while it is being used by a ruleset. */
 	set_cap(_metadata, CAP_SYS_ADMIN);
@@ -7612,11 +7269,7 @@ TEST_F(audit_layout1, execute_make)
 	test_execute(_metadata, 0, file1_s1d1);
 	test_check_exec(_metadata, 0, file1_s1d1);
 
-	drop_access_rights(_metadata,
-			   &(struct landlock_ruleset_attr){
-				   .handled_access_fs =
-					   LANDLOCK_ACCESS_FS_EXECUTE,
-			   });
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_EXECUTE, NULL);
 
 	test_execute(_metadata, EACCES, file1_s1d1);
 	EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.execute",
@@ -7664,9 +7317,7 @@ TEST_F(audit_layout1, execute_read)
 	test_execute(_metadata, 0, file1_s1d1);
 	test_check_exec(_metadata, 0, file1_s1d1);
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
-				      });
+	enforce_fs(_metadata, access_fs_16, NULL);
 
 	/*
 	 * The only difference with the previous audit_layout1.execute_read test is
@@ -7688,9 +7339,7 @@ TEST_F(audit_layout1, write_file)
 {
 	struct audit_records records;
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
-				      });
+	enforce_fs(_metadata, access_fs_16, NULL);
 
 	EXPECT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY));
 	EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
@@ -7705,9 +7354,7 @@ TEST_F(audit_layout1, read_file)
 {
 	struct audit_records records;
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
-				      });
+	enforce_fs(_metadata, access_fs_16, NULL);
 
 	EXPECT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
 	EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.read_file",
@@ -7722,9 +7369,7 @@ TEST_F(audit_layout1, read_dir)
 {
 	struct audit_records records;
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
-				      });
+	enforce_fs(_metadata, access_fs_16, NULL);
 
 	EXPECT_EQ(EACCES, test_open(dir_s1d1, O_DIRECTORY));
 	EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.read_dir",
@@ -7742,9 +7387,7 @@ TEST_F(audit_layout1, remove_dir)
 	EXPECT_EQ(0, unlink(file1_s1d3));
 	EXPECT_EQ(0, unlink(file2_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
-				      });
+	enforce_fs(_metadata, access_fs_16, NULL);
 
 	EXPECT_EQ(-1, rmdir(dir_s1d3));
 	EXPECT_EQ(EACCES, errno);
@@ -7765,9 +7408,7 @@ TEST_F(audit_layout1, remove_file)
 {
 	struct audit_records records;
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
-				      });
+	enforce_fs(_metadata, access_fs_16, NULL);
 
 	EXPECT_EQ(-1, unlink(file1_s1d3));
 	EXPECT_EQ(EACCES, errno);
@@ -7785,9 +7426,7 @@ TEST_F(audit_layout1, make_char)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
-				      });
+	enforce_fs(_metadata, access_fs_16, NULL);
 
 	EXPECT_EQ(-1, mknod(file1_s1d3, S_IFCHR | 0644, 0));
 	EXPECT_EQ(EACCES, errno);
@@ -7805,9 +7444,7 @@ TEST_F(audit_layout1, make_dir)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
-				      });
+	enforce_fs(_metadata, access_fs_16, NULL);
 
 	EXPECT_EQ(-1, mkdir(file1_s1d3, 0755));
 	EXPECT_EQ(EACCES, errno);
@@ -7825,9 +7462,7 @@ TEST_F(audit_layout1, make_reg)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
-				      });
+	enforce_fs(_metadata, access_fs_16, NULL);
 
 	EXPECT_EQ(-1, mknod(file1_s1d3, S_IFREG | 0644, 0));
 	EXPECT_EQ(EACCES, errno);
@@ -7845,9 +7480,7 @@ TEST_F(audit_layout1, make_sock)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
-				      });
+	enforce_fs(_metadata, access_fs_16, NULL);
 
 	EXPECT_EQ(-1, mknod(file1_s1d3, S_IFSOCK | 0644, 0));
 	EXPECT_EQ(EACCES, errno);
@@ -7865,9 +7498,7 @@ TEST_F(audit_layout1, make_fifo)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
-				      });
+	enforce_fs(_metadata, access_fs_16, NULL);
 
 	EXPECT_EQ(-1, mknod(file1_s1d3, S_IFIFO | 0644, 0));
 	EXPECT_EQ(EACCES, errno);
@@ -7885,9 +7516,7 @@ TEST_F(audit_layout1, make_block)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
-				      });
+	enforce_fs(_metadata, access_fs_16, NULL);
 
 	EXPECT_EQ(-1, mknod(file1_s1d3, S_IFBLK | 0644, 0));
 	EXPECT_EQ(EACCES, errno);
@@ -7905,9 +7534,7 @@ TEST_F(audit_layout1, make_sym)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
-				      });
+	enforce_fs(_metadata, access_fs_16, NULL);
 
 	EXPECT_EQ(-1, symlink("target", file1_s1d3));
 	EXPECT_EQ(EACCES, errno);
@@ -7925,10 +7552,7 @@ TEST_F(audit_layout1, refer_handled)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs =
-						      LANDLOCK_ACCESS_FS_REFER,
-				      });
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_REFER, NULL);
 
 	EXPECT_EQ(-1, link(file1_s1d1, file1_s1d3));
 	EXPECT_EQ(EXDEV, errno);
@@ -7950,12 +7574,9 @@ TEST_F(audit_layout1, refer_make)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata,
-			   &(struct landlock_ruleset_attr){
-				   .handled_access_fs =
-					   LANDLOCK_ACCESS_FS_MAKE_REG |
-					   LANDLOCK_ACCESS_FS_REFER,
-			   });
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER,
+		   NULL);
 
 	EXPECT_EQ(-1, link(file1_s1d1, file1_s1d3));
 	EXPECT_EQ(EACCES, errno);
@@ -7975,9 +7596,7 @@ TEST_F(audit_layout1, refer_rename)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
-				      });
+	enforce_fs(_metadata, access_fs_16, NULL);
 
 	EXPECT_EQ(EACCES, test_rename(file1_s1d2, file1_s2d3));
 	EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
@@ -7997,9 +7616,7 @@ TEST_F(audit_layout1, refer_exchange)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
-				      });
+	enforce_fs(_metadata, access_fs_16, NULL);
 
 	/*
 	 * The only difference with the previous audit_layout1.refer_rename test is
@@ -8037,12 +7654,8 @@ TEST_F(audit_layout1, refer_rename_half)
 		},
 		{},
 	};
-	int ruleset_fd =
-		create_ruleset(_metadata, LANDLOCK_ACCESS_FS_REFER, layer1);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_REFER, layer1);
 
 	ASSERT_EQ(-1, rename(dir_s1d2, dir_s2d3));
 	ASSERT_EQ(EXDEV, errno);
@@ -8060,9 +7673,7 @@ TEST_F(audit_layout1, truncate)
 {
 	struct audit_records records;
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
-				      });
+	enforce_fs(_metadata, access_fs_16, NULL);
 
 	EXPECT_EQ(-1, truncate(file1_s1d3, 0));
 	EXPECT_EQ(EACCES, errno);
@@ -8079,12 +7690,8 @@ TEST_F(audit_layout1, ioctl_dev)
 	struct audit_records records;
 	int fd;
 
-	drop_access_rights(_metadata,
-			   &(struct landlock_ruleset_attr){
-				   .handled_access_fs =
-					   access_fs_16 &
-					   ~LANDLOCK_ACCESS_FS_READ_FILE,
-			   });
+	enforce_fs(_metadata, access_fs_16 & ~LANDLOCK_ACCESS_FS_READ_FILE,
+		   NULL);
 
 	fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
 	ASSERT_LE(0, fd);
@@ -8110,10 +7717,7 @@ TEST_F(audit_layout1, resolve_unix)
 	child_pid = fork();
 	ASSERT_LE(0, child_pid);
 	if (!child_pid) {
-		drop_access_rights(_metadata,
-				   &(struct landlock_ruleset_attr){
-					   .handled_access_fs = access_fs_16,
-				   });
+		enforce_fs(_metadata, access_fs_16, NULL);
 
 		cli_fd = socket(AF_UNIX, SOCK_STREAM, 0);
 		ASSERT_LE(0, cli_fd);
@@ -8141,11 +7745,7 @@ TEST_F(audit_layout1, mount)
 {
 	struct audit_records records;
 
-	drop_access_rights(_metadata,
-			   &(struct landlock_ruleset_attr){
-				   .handled_access_fs =
-					   LANDLOCK_ACCESS_FS_EXECUTE,
-			   });
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_EXECUTE, NULL);
 
 	set_cap(_metadata, CAP_SYS_ADMIN);
 	EXPECT_EQ(-1, mount(NULL, dir_s3d2, NULL, MS_RDONLY, NULL));
Signature · 1 line
2.52.0
#38 lore raw

PATCH v5 8/9 landlock: Document FS access right for pathname UNIX sockets

In reply to Günther Noack
Cc Justin Suess <[email protected]>
Cc Mickaël Salaün <[email protected]>
---
 Documentation/userspace-api/landlock.rst | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)
Documentation/userspace-api/landlock.rst +15 -1
index 13134bccdd39..3ba73afcbc4b 100644
@@ -77,7 +77,8 @@ to be explicit about the denied-by-default access rights.
             LANDLOCK_ACCESS_FS_MAKE_SYM |
             LANDLOCK_ACCESS_FS_REFER |
             LANDLOCK_ACCESS_FS_TRUNCATE |
-            LANDLOCK_ACCESS_FS_IOCTL_DEV,
+            LANDLOCK_ACCESS_FS_IOCTL_DEV |
+            LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
         .handled_access_net =
             LANDLOCK_ACCESS_NET_BIND_TCP |
             LANDLOCK_ACCESS_NET_CONNECT_TCP,
@@ -127,6 +128,12 @@ 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:
+        __attribute__((fallthrough));
+    case 8:
+        /* Removes LANDLOCK_ACCESS_FS_RESOLVE_UNIX for ABI < 8 */
+        ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_RESOLVE_UNIX;
     }
 
 This enables the creation of an inclusive ruleset that will contain our rules.
@@ -685,6 +692,13 @@ enforce Landlock rulesets across all threads of the calling process
 using the ``LANDLOCK_RESTRICT_SELF_TSYNC`` flag passed to
 sys_landlock_restrict_self().
 
+Pathname UNIX sockets (ABI < 9)
+-------------------------------
+
+Starting with the Landlock ABI version 9, it is possible to restrict
+connections to pathname UNIX domain sockets (:manpage:`unix(7)`) using
+the new ``LANDLOCK_ACCESS_FS_RESOLVE_UNIX`` right.
+
 .. _kernel_support:
 
 Kernel support
Signature · 1 line
2.52.0
maintainer
#39 lore raw

Re: [PATCH v5 8/9] landlock: Document FS access right for pathname UNIX sockets

Quoted 22 lines · Günther Noack
Cc Justin Suess <[email protected]>
Cc Mickaël Salaün <[email protected]>
---
 Documentation/userspace-api/landlock.rst | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)
Documentation/userspace-api/landlock.rst +2 -1
index 13134bccdd39..3ba73afcbc4b 100644
@@ -77,7 +77,8 @@ to be explicit about the denied-by-default access rights.
             LANDLOCK_ACCESS_FS_MAKE_SYM |
             LANDLOCK_ACCESS_FS_REFER |
             LANDLOCK_ACCESS_FS_TRUNCATE |
-            LANDLOCK_ACCESS_FS_IOCTL_DEV,
+            LANDLOCK_ACCESS_FS_IOCTL_DEV |
+            LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
         .handled_access_net =
             LANDLOCK_ACCESS_NET_BIND_TCP |
             LANDLOCK_ACCESS_NET_CONNECT_TCP,

Documentation/userspace-api/landlock.rst version, and only use the available subset of access rights: · line 133

Quoted 6 lines
Documentation/userspace-api/landlock.rst +3 -0
@@ -127,6 +128,12 @@ 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:
+        __attribute__((fallthrough));

I don't think the fallthrough attribute is needed here. Same for the sample.

Documentation/userspace-api/landlock.rst version, and only use the available subset of access rights: · line 135

Quoted 2 lines
Documentation/userspace-api/landlock.rst +2 -0
+    case 8:
+        /* Removes LANDLOCK_ACCESS_FS_RESOLVE_UNIX for ABI < 8 */

ABI < 9

Quoted 22 lines
Documentation/userspace-api/landlock.rst +8 -0
+        ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_RESOLVE_UNIX;
     }
 
 This enables the creation of an inclusive ruleset that will contain our rules.
@@ -685,6 +692,13 @@ enforce Landlock rulesets across all threads of the calling process
 using the ``LANDLOCK_RESTRICT_SELF_TSYNC`` flag passed to
 sys_landlock_restrict_self().
 
+Pathname UNIX sockets (ABI < 9)
+-------------------------------
+
+Starting with the Landlock ABI version 9, it is possible to restrict
+connections to pathname UNIX domain sockets (:manpage:`unix(7)`) using
+the new ``LANDLOCK_ACCESS_FS_RESOLVE_UNIX`` right.
+
 .. _kernel_support:
 
 Kernel support
Signature · 1 line
2.52.0
#40 lore raw

Re: [PATCH v5 8/9] landlock: Document FS access right for pathname UNIX sockets

Quoted 15 lines · Mickaël Salaün
Quoted 14 lines · Günther Noack
Documentation/userspace-api/landlock.rst +2 -1
@@ -77,7 +77,8 @@ to be explicit about the denied-by-default access rights.
             LANDLOCK_ACCESS_FS_MAKE_SYM |
             LANDLOCK_ACCESS_FS_REFER |
             LANDLOCK_ACCESS_FS_TRUNCATE |
-            LANDLOCK_ACCESS_FS_IOCTL_DEV,
+            LANDLOCK_ACCESS_FS_IOCTL_DEV |
+            LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
         .handled_access_net =
             LANDLOCK_ACCESS_NET_BIND_TCP |
             LANDLOCK_ACCESS_NET_CONNECT_TCP,
@@ -127,6 +128,12 @@ version, and only use the available subset of access rights:
         /* Removes LANDLOCK_SCOPE_* for ABI < 6 */
Quoted 8 lines

Documentation/userspace-api/landlock.rst version, and only use the available subset of access rights: · line 133

Quoted 5 lines
Documentation/userspace-api/landlock.rst +3 -0
         ruleset_attr.scoped &= ~(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
                                  LANDLOCK_SCOPE_SIGNAL);
+        __attribute__((fallthrough));
+    case 7:
+        __attribute__((fallthrough));

I don't think the fallthrough attribute is needed here. Same for the sample.

Thanks, done.

Quoted 4 lines

Documentation/userspace-api/landlock.rst version, and only use the available subset of access rights: · line 135

Quoted 2 lines
Documentation/userspace-api/landlock.rst +2 -0
+    case 8:
+        /* Removes LANDLOCK_ACCESS_FS_RESOLVE_UNIX for ABI < 8 */

ABI < 9

Good catch, done.

–Günther

#41 lore raw

PATCH v5 9/9 landlock: Document design rationale for scoped access rights

In reply to Günther Noack

Document the (possible future) interaction between scoped flags and other access rights in struct landlock_ruleset_attr, and summarize the rationale, as discussed in code review leading up to [1].

Link[1]: https://lore.kernel.org/all/[email protected]/

---
 Documentation/security/landlock.rst | 38 +++++++++++++++++++++++++++++
 1 file changed, 38 insertions(+)
Documentation/security/landlock.rst +38 -0
index 3e4d4d04cfae..49ef02d5e272 100644
@@ -89,6 +89,44 @@ this is required to keep access controls consistent over the whole system, and
 this avoids unattended bypasses through file descriptor passing (i.e. confused
 deputy attack).
 
+Interaction between scoped flags and other access rights
+--------------------------------------------------------
+
+The ``scoped`` flags in ``struct landlock_ruleset_attr`` restrict the
+use of *outgoing* IPC from the created Landlock domain, while they
+permit reaching out to IPC endpoints *within* the created Landlock
+domain.
+
+In the future, scoped flags *may* interact with other access rights,
+e.g. so that abstract UNIX sockets can be allow-listed by name, or so
+that signals can be allow-listed by signal number or target process.
+
+When introducing ``LANDLOCK_ACCESS_FS_RESOLVE_UNIX``, we defined it to
+implicitly have the same scoping semantics as a
+``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` flag would have: connecting to
+UNIX sockets within the same domain (where
+``LANDLOCK_ACCESS_FS_RESOLVE_UNIX`` is used) is unconditionally
+allowed.
+
+The reasoning is:
+
+* Like other IPC mechanisms, connecting to named UNIX sockets in the
+  same domain should be expected and harmless.  (If needed, users can
+  further refine their Landlock policies with nested domains or by
+  restricting ``LANDLOCK_ACCESS_FS_MAKE_SOCK``.)
+* We reserve the option to still introduce
+  ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` in the future.  (This would
+  be useful if we wanted to have a Landlock rule to permit IPC access
+  to other Landlock domains.)
+* But we can postpone the point in time when users have to deal with
+  two interacting flags visible in the userspace API.  (In particular,
+  it is possible that it won't be needed in practice, in which case we
+  can avoid the second flag altogether.)
+* If we *do* introduce ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` in the
+  future, setting this scoped flag in a ruleset does *not reduce* the
+  restrictions, because access within the same scope is already
+  allowed based on ``LANDLOCK_ACCESS_FS_RESOLVE_UNIX``.
+
 Tests
 =====
 
Signature · 1 line
2.52.0
Reviewed-by on v5 9/9
#42 lore raw

Re: [PATCH v5 9/9] landlock: Document design rationale for scoped access rights

Quoted 9 lines · Günther Noack

Document the (possible future) interaction between scoped flags and other access rights in struct landlock_ruleset_attr, and summarize the rationale, as discussed in code review leading up to [1].

Link[1]: https://lore.kernel.org/all/[email protected]/

---
 Documentation/security/landlock.rst | 38 +++++++++++++++++++++++++++++
 1 file changed, 38 insertions(+)