Landlock: Implement scope control for pathname Unix sockets
Subsystems (6)
v2 cover Landlock: Implement scope control for pathname Unix sockets
Changes in v2:
Fix grammar in doc, rebased on mic/next, and extracted common code from hook_unix_stream_connect and hook_unix_may_send into a separate function.
The rest is the same as the v1 cover letter:
This patch series extend the existing abstract Unix socket scoping to pathname (i.e. normal file-based) sockets as well, by adding a new scope bit LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET that works the same as LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, except that restricts pathname Unix sockets. This means that a sandboxed process with this scope enabled will not be able to connect to Unix sockets created outside the sandbox via the filesystem.
There is a future plan [1] for allowing specific sockets based on FS hierarchy, but this series is only determining access based on domain parent-child relationship. There is currently no way to allow specific (outside the Landlock domain) Unix sockets, and none of the existing Landlock filesystem controls apply to socket connect().
With this series, we can now properly protect against things like the the following while only relying on Landlock:
(running under tmux)
root@6-19-0-rc1-dev-00023-g68f0b276cbeb ~# LL_FS_RO=/ LL_FS_RW= ./sandboxer bash
Executing the sandboxed command...
root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi
cat: /tmp/hi: No such file or directory
root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# tmux new-window 'echo hi > /tmp/hi'
root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi
hiThe above but with Unix socket scoping enabled (both pathname and abstract sockets) - the sandboxed shell can now no longer talk to tmux due to the socket being created from outside the Landlock sandbox:
(running under tmux)
root@6-19-0-rc1-dev-00023-g68f0b276cbeb ~# LL_FS_RO=/ LL_FS_RW= LL_SCOPED=u:a ./sandboxer bash Executing the sandboxed command...
root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi
cat: /tmp/hi: No such file or directory root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# tmux new-window 'echo hi > /tmp/hi' error connecting to /tmp/tmux-0/default (Operation not permitted) root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi
cat: /tmp/hi: No such file or directory
Tmux is just one example. In a standard systemd session, systemd-run --user can also be used (--user will run the command in the user's session, without requiring any root privileges), and likely a lot more if running in a desktop environment with many popular applications. This change therefore makes it possible to create sandboxes without relying on additional mechanisms like seccomp to protect against such issues.
These kind of issues was originally discussed on here (I took the idea for systemd-run from Demi):
https://spectrum-os.org/lists/archives/spectrum-devel/[email protected]/
Demo with socat + sandboxer:
Outside:
socat unix-listen:/foo.sock,fork -
Sandbox with pathname socket scope bit:
root@6-19-0-rc1-dev-00023-g0994a10d6512 ~# LL_FS_RW=/ LL_FS_RO= LL_SCOPED=u /sandboxer socat -d2 unix:/foo.sock -
Executing the sandboxed command...
2025/12/27 20:28:54 socat[1227] E UNIX-CLIENT: /foo.sock: Operation not permitted
2025/12/27 20:28:54 socat[1227] N exit(1)Sandbox without pathname socket scope bit:
root@6-19-0-rc1-dev-00023-g0994a10d6512 ~# LL_FS_RW=/ LL_FS_RO= LL_SCOPED= /sandboxer socat -d2 unix:/foo.sock -
Executing the sandboxed command...
2025/12/27 20:29:22 socat[1250] N successfully connected from local address AF=1 "(7\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xB0\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xAE\xC3\xAE\xAE\xAE\xAE"
...Sandbox with only abstract socket scope bit:
root@6-19-0-rc1-dev-00023-g0994a10d6512 ~# LL_FS_RW=/ LL_FS_RO= LL_SCOPED=a /sandboxer socat -d2 unix:/foo.sock -
Executing the sandboxed command...
2025/12/27 20:29:26 socat[1259] N successfully connected from local address AF=1 "\0\0\0\0\0\0\0\0\0"
...Sendmsg/recvmsg - outside:
socat unix-recvfrom:/datagram.sock -
Sandbox with pathname socket scope bit:
root@6-19-0-rc1-dev-00023-g0994a10d6512 ~# LL_FS_RW=/ LL_FS_RO= LL_SCOPED=u /sandboxer socat -d2 unix-sendto:/datagram.sock -
Executing the sandboxed command...
...
2025/12/27 20:33:04 socat[1446] N starting data transfer loop with FDs [5,5] and [0,1]
123
2025/12/27 20:33:05 socat[1446] E sendto(5, 0x55d260d8f000, 4, 0, AF=1 "/datagram.sock", 16): Operation not permitted
2025/12/27 20:33:05 socat[1446] N exit(1)[1]: https://github.com/landlock-lsm/linux/issues/36
Tingmao Wang (6):
landlock: Add LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET scope bit to uAPI
landlock: Implement LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET samples/landlock: Support LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET selftests/landlock: Support pathname socket path in set_unix_address selftests/landlock: Repurpose scoped_abstract_unix_test.c for pathname
sockets too.
selftests/landlock: Add pathname socket variants for more tests
Documentation/userspace-api/landlock.rst | 37 +-
include/uapi/linux/landlock.h | 8 +-
samples/landlock/sandboxer.c | 23 +-
security/landlock/audit.c | 4 +
security/landlock/audit.h | 1 +
security/landlock/limits.h | 2 +-
security/landlock/syscalls.c | 2 +-
security/landlock/task.c | 109 ++-
tools/testing/selftests/landlock/base_test.c | 2 +-
tools/testing/selftests/landlock/common.h | 33 +-
tools/testing/selftests/landlock/net_test.c | 2 +-
.../selftests/landlock/scoped_signal_test.c | 2 +-
.../testing/selftests/landlock/scoped_test.c | 2 +-
...bstract_unix_test.c => scoped_unix_test.c} | 855 ++++++++++++------
14 files changed, 757 insertions(+), 325 deletions(-)
rename tools/testing/selftests/landlock/{scoped_abstract_unix_test.c => scoped_unix_test.c} (51%)base-commit: ef4536f15224418b327a7b5d5cae07dab042760f
Signature · 1 line
2.52.0
PATCH v2 1/6 landlock: Add LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET scope bit to uAPI
Add the new scope bit to the uAPI header, add documentation, and bump ABI version to 8.
This documentation edit specifically calls out the security implications of not restricting sockets.
Fix some minor cosmetic issue in landlock.h around the changed lines as well.
---
Changes in v2:
- Fix grammar
Note that in the code block in "Defining and enforcing a security policy" the switch case currently jumps from 5 to 7. This should be fixed by
https://lore.kernel.org/all/[email protected]/
Documentation/userspace-api/landlock.rst | 37 ++++++++++++++++--- include/uapi/linux/landlock.h | 8 +++- security/landlock/limits.h | 2 +- security/landlock/syscalls.c | 2 +- tools/testing/selftests/landlock/base_test.c | 2 +- .../testing/selftests/landlock/scoped_test.c | 2 +- 6 files changed, 42 insertions(+), 11 deletions(-)
Documentation/userspace-api/landlock.rst +32 -5
@@ -83,7 +83,8 @@ to be explicit about the denied-by-default access rights. LANDLOCK_ACCESS_NET_CONNECT_TCP, .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | - LANDLOCK_SCOPE_SIGNAL, + LANDLOCK_SCOPE_SIGNAL | + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET, }; Because we may not know which kernel version an application will be executed
@@ -127,6 +128,10 @@ version, and only use the available subset of access rights: /* Removes LANDLOCK_SCOPE_* for ABI < 6 */ ruleset_attr.scoped &= ~(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL); + __attribute__((fallthrough)); + case 7: + /* Removes LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET for ABI < 8 */ + ruleset_attr.scoped &= ~LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET; } This enables the creation of an inclusive ruleset that will contain our rules.
@@ -328,10 +333,15 @@ The operations which can be scoped are: This limits the sending of signals to target processes which run within the same or a nested Landlock domain. -``LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET`` - This limits the set of abstract :manpage:`unix(7)` sockets to which we can - :manpage:`connect(2)` to socket addresses which were created by a process in - the same or a nested Landlock domain. +``LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET`` and ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` + This limits the set of :manpage:`unix(7)` sockets to which we can + :manpage:`connect(2)` to socket addresses which were created by a + process in the same or a nested Landlock domain. + ``LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET`` applies to abstract sockets, + and ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` applies to pathname + sockets. Even though pathname sockets are represented in the + filesystem, Landlock filesystem rules do not currently control access + to them. A :manpage:`sendto(2)` on a non-connected datagram socket is treated as if it were doing an implicit :manpage:`connect(2)` and will be blocked if the
@@ -604,6 +614,23 @@ Landlock audit events with the ``LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF``, sys_landlock_restrict_self(). See Documentation/admin-guide/LSM/landlock.rst for more details on audit. +Pathname UNIX socket (ABI < 8) +------------------------------ + +Starting with the Landlock ABI version 8, it is possible to restrict +connections to a pathname (non-abstract) :manpage:`unix(7)` socket by +setting ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` to the ``scoped`` ruleset +attribute. This works the same way as the abstract socket scoping. + +This allows sandboxing applications using only Landlock to protect against +bypasses relying on connecting to Unix sockets of other services running +under the same user. These services typically assume that any process +capable of connecting to a local Unix socket, or connecting with the +expected user credentials, is trusted. Without this protection, sandbox +escapes may be possible, especially when running in a standard desktop +environment, such as by using systemd-run, or sockets exposed by other +common applications. + .. _kernel_support: Kernel support
include/uapi/linux/landlock.h +6 -2
@@ -364,10 +364,14 @@ struct landlock_net_port_attr {* related Landlock domain (e.g., a parent domain or a non-sandboxed process). * - %LANDLOCK_SCOPE_SIGNAL: Restrict a sandboxed process from sending a signal * to another process outside the domain. +* - %LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET: Restrict a sandboxed process from +* connecting to a pathname UNIX socket created by a process outside the +* related Landlock domain. */ /* clang-format off */ #define LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET (1ULL << 0) -#define LANDLOCK_SCOPE_SIGNAL (1ULL << 1) -/* clang-format on*/ +#define LANDLOCK_SCOPE_SIGNAL (1ULL << 1) +#define LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET (1ULL << 2) +/* clang-format on */ #endif /* _UAPI_LINUX_LANDLOCK_H */
security/landlock/limits.h +1 -1
@@ -27,7 +27,7 @@#define LANDLOCK_MASK_ACCESS_NET ((LANDLOCK_LAST_ACCESS_NET << 1) - 1)#define LANDLOCK_NUM_ACCESS_NET __const_hweight64(LANDLOCK_MASK_ACCESS_NET) -#define LANDLOCK_LAST_SCOPE LANDLOCK_SCOPE_SIGNAL +#define LANDLOCK_LAST_SCOPE LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET#define LANDLOCK_MASK_SCOPE ((LANDLOCK_LAST_SCOPE << 1) - 1)#define LANDLOCK_NUM_SCOPE __const_hweight64(LANDLOCK_MASK_SCOPE)
security/landlock/syscalls.c +1 -1
@@ -161,7 +161,7 @@ static const struct file_operations ruleset_fops = {* Documentation/userspace-api/landlock.rst should be updated to reflect the * UAPI change. */ -const int landlock_abi_version =7 ; +const int landlock_abi_version =8 ;/** * sys_landlock_create_ruleset - Create a new ruleset
tools/testing/selftests/landlock/base_test.c +1 -1
@@ -76,7 +76,7 @@ TEST(abi_version)const struct landlock_ruleset_attr ruleset_attr = { .handled_access_fs =LANDLOCK_ACCESS_FS_READ_FILE , }; -ASSERT_EQ (7 ,landlock_create_ruleset (NULL ,0 , +ASSERT_EQ (8 ,landlock_create_ruleset (NULL ,0 ,LANDLOCK_CREATE_RULESET_VERSION )); ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
tools/testing/selftests/landlock/scoped_test.c +1 -1
@@ -12,7 +12,7 @@#include "common.h" -#define ACCESS_LAST LANDLOCK_SCOPE_SIGNAL +#define ACCESS_LAST LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETTEST (ruleset_with_unknown_scope) {
Signature · 1 line
2.52.0
Mickaël Salaün
maintainerRe: [PATCH v2 1/6] landlock: Add LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET scope bit to uAPI
Quoted 2 lines · Tingmao Wang
Add the new scope bit to the uAPI header, add documentation, and bump ABI version to 8.
This patch and the next one should be fold together. If a new UAPI is added, it should come with the kernel implementation.
Quoted 55 lines
This documentation edit specifically calls out the security implications of not restricting sockets.
Fix some minor cosmetic issue in
landlock.haround the changed lines as well.Signed-off-by Tingmao Wang <[email protected]>---
Changes in v2:
- Fix grammarNote that in the code block in "Defining and enforcing a security policy" the switch case currently jumps from 5 to 7. This should be fixed by
https://lore.kernel.org/all/[email protected]/Documentation/userspace-api/landlock.rst | 37 ++++++++++++++++--- include/uapi/linux/landlock.h | 8 +++- security/landlock/limits.h | 2 +- security/landlock/syscalls.c | 2 +- tools/testing/selftests/landlock/base_test.c | 2 +- .../testing/selftests/landlock/scoped_test.c | 2 +- 6 files changed, 42 insertions(+), 11 deletions(-)Documentation/userspace-api/landlock.rst +6
-2@@ -83,7 +83,8 @@ to be explicit about the denied-by-default access rights. LANDLOCK_ACCESS_NET_CONNECT_TCP, .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | - LANDLOCK_SCOPE_SIGNAL, + LANDLOCK_SCOPE_SIGNAL | + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET, }; Because we may not know which kernel version an application will be executed@@ -127,6 +128,10 @@ version, and only use the available subset of access rights: /* Removes LANDLOCK_SCOPE_* for ABI < 6 */ ruleset_attr.scoped &= ~(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL); + __attribute__((fallthrough)); + case 7: + /* Removes LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET for ABI < 8 */ + ruleset_attr.scoped &= ~LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET; } This enables the creation of an inclusive ruleset that will contain our rules.@@ -328,10 +333,15 @@ The operations which can be scoped are: This limits the sending of signals to target processes which run within the same or a nested Landlock domain. -``LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET``
Documentation/userspace-api/landlock.rst The operations which can be scoped are: · line 342
Quoted 10 lines
Documentation/userspace-api/landlock.rst +7
-3- This limits the set of abstract :manpage:`unix(7)` sockets to which we can - :manpage:`connect(2)` to socket addresses which were created by a process in - the same or a nested Landlock domain. +``LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET`` and ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` + This limits the set of :manpage:`unix(7)` sockets to which we can + :manpage:`connect(2)` to socket addresses which were created by a + process in the same or a nested Landlock domain. + ``LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET`` applies to abstract sockets, + and ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` applies to pathname + sockets.
The following part is not needed:
Quoted 107 lines
Even though pathname sockets are represented in the
Documentation/userspace-api/landlock.rst +19
-0+ filesystem, Landlock filesystem rules do not currently control access + to them. A :manpage:`sendto(2)` on a non-connected datagram socket is treated as if it were doing an implicit :manpage:`connect(2)` and will be blocked if the@@ -604,6 +614,23 @@ Landlock audit events with the ``LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF``, sys_landlock_restrict_self(). See Documentation/admin-guide/LSM/landlock.rst for more details on audit. +Pathname UNIX socket (ABI < 8) +------------------------------ + +Starting with the Landlock ABI version 8, it is possible to restrict +connections to a pathname (non-abstract) :manpage:`unix(7)` socket by +setting ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` to the ``scoped`` ruleset +attribute. This works the same way as the abstract socket scoping. + +This allows sandboxing applications using only Landlock to protect against +bypasses relying on connecting to Unix sockets of other services running +under the same user. These services typically assume that any process +capable of connecting to a local Unix socket, or connecting with the +expected user credentials, is trusted. Without this protection, sandbox +escapes may be possible, especially when running in a standard desktop +environment, such as by using systemd-run, or sockets exposed by other +common applications. + .. _kernel_support: Kernel supportinclude/uapi/linux/landlock.h +6
-2@@ -364,10 +364,14 @@ struct landlock_net_port_attr {* related Landlock domain (e.g., a parent domain or a non-sandboxed process). * - %LANDLOCK_SCOPE_SIGNAL: Restrict a sandboxed process from sending a signal * to another process outside the domain. +* - %LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET: Restrict a sandboxed process from +* connecting to a pathname UNIX socket created by a process outside the +* related Landlock domain. */ /* clang-format off */ #define LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET (1ULL << 0) -#define LANDLOCK_SCOPE_SIGNAL (1ULL << 1) -/* clang-format on*/ +#define LANDLOCK_SCOPE_SIGNAL (1ULL << 1) +#define LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET (1ULL << 2) +/* clang-format on */ #endif /* _UAPI_LINUX_LANDLOCK_H */ security/landlock/limits.h +1
-1@@ -27,7 +27,7 @@#define LANDLOCK_MASK_ACCESS_NET ((LANDLOCK_LAST_ACCESS_NET << 1) - 1)#define LANDLOCK_NUM_ACCESS_NET __const_hweight64(LANDLOCK_MASK_ACCESS_NET) -#define LANDLOCK_LAST_SCOPE LANDLOCK_SCOPE_SIGNAL +#define LANDLOCK_LAST_SCOPE LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET#define LANDLOCK_MASK_SCOPE ((LANDLOCK_LAST_SCOPE << 1) - 1)#define LANDLOCK_NUM_SCOPE __const_hweight64(LANDLOCK_MASK_SCOPE)security/landlock/syscalls.c +1
-1@@ -161,7 +161,7 @@ static const struct file_operations ruleset_fops = {* Documentation/userspace-api/landlock.rst should be updated to reflect the * UAPI change. */ -const int landlock_abi_version =7 ; +const int landlock_abi_version =8 ;/** * sys_landlock_create_ruleset - Create a new ruleset tools/testing/selftests/landlock/base_test.c +1
-1@@ -76,7 +76,7 @@ TEST(abi_version)const struct landlock_ruleset_attr ruleset_attr = { .handled_access_fs =LANDLOCK_ACCESS_FS_READ_FILE , }; -ASSERT_EQ (7 ,landlock_create_ruleset (NULL ,0 , +ASSERT_EQ (8 ,landlock_create_ruleset (NULL ,0 ,LANDLOCK_CREATE_RULESET_VERSION )); ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,tools/testing/selftests/landlock/scoped_test.c +1
-1@@ -12,7 +12,7 @@#include "common.h" -#define ACCESS_LAST LANDLOCK_SCOPE_SIGNAL +#define ACCESS_LAST LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETTEST (ruleset_with_unknown_scope) {Signature · 1 line
2.52.0
PATCH v2 2/6 landlock: Implement LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
In reply to Tingmao WangExtend the existing abstract UNIX socket scoping to pathname sockets as well. Basically all of the logic is reused between the two types, just that pathname sockets scoping are controlled by another bit, and has its own audit request type (since the current one is named "abstract_unix_socket").
---
Changes in v2:
- Factor out common code in hook_unix_stream_connect and hook_unix_may_send into check_socket_access(), and inline is_abstract_socket().
security/landlock/audit.c | 4 ++ security/landlock/audit.h | 1 + security/landlock/task.c | 109 ++++++++++++++++++++++---------------- 3 files changed, 67 insertions(+), 47 deletions(-)
security/landlock/audit.c +4 -0
@@ -75,6 +75,10 @@ get_blocker(const enum landlock_request_type type,WARN_ON_ONCE (access_bit !=-1 );return "scope.abstract_unix_socket" ; +case LANDLOCK_REQUEST_SCOPE_PATHNAME_UNIX_SOCKET : +WARN_ON_ONCE (access_bit !=-1 ); +return "scope.pathname_unix_socket" ; +case LANDLOCK_REQUEST_SCOPE_SIGNAL :WARN_ON_ONCE (access_bit !=-1 );return "scope.signal" ;
security/landlock/audit.h +1 -0
@@ -21,6 +21,7 @@ enum landlock_request_type {LANDLOCK_REQUEST_NET_ACCESS ,LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET ,LANDLOCK_REQUEST_SCOPE_SIGNAL , + LANDLOCK_REQUEST_SCOPE_PATHNAME_UNIX_SOCKET, };/*
security/landlock/task.c +62 -47
149 lines not on this page · raw message
Signature · 1 line
2.52.0
Mickaël Salaün
maintainerRe: [PATCH v2 2/6] landlock: Implement LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
Quoted 72 lines · Tingmao Wang
Extend the existing abstract UNIX socket scoping to pathname sockets as well. Basically all of the logic is reused between the two types, just that pathname sockets scoping are controlled by another bit, and has its own audit request type (since the current one is named "
abstract_unix_socket").Signed-off-by Tingmao Wang <[email protected]>---
Changes in v2:
- Factor out common code inhook_unix_stream_connectandhook_unix_may_sendintocheck_socket_access(), and inlineis_abstract_socket().security/landlock/audit.c | 4 ++ security/landlock/audit.h | 1 + security/landlock/task.c | 109 ++++++++++++++++++++++---------------- 3 files changed, 67 insertions(+), 47 deletions(-)security/landlock/audit.c +4
-0@@ -75,6 +75,10 @@ get_blocker(const enum landlock_request_type type,WARN_ON_ONCE (access_bit !=-1 );return "scope.abstract_unix_socket" ; +case LANDLOCK_REQUEST_SCOPE_PATHNAME_UNIX_SOCKET : +WARN_ON_ONCE (access_bit !=-1 ); +return "scope.pathname_unix_socket" ; +case LANDLOCK_REQUEST_SCOPE_SIGNAL :WARN_ON_ONCE (access_bit !=-1 );return "scope.signal" ;security/landlock/audit.h +1
-0@@ -21,6 +21,7 @@ enum landlock_request_type {LANDLOCK_REQUEST_NET_ACCESS ,LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET ,LANDLOCK_REQUEST_SCOPE_SIGNAL , + LANDLOCK_REQUEST_SCOPE_PATHNAME_UNIX_SOCKET, };/* security/landlock/task.c +12
-1@@ -232,35 +232,81 @@ static bool domain_is_scoped(const struct landlock_ruleset *const client,return false; } +/** +* sock_is_scoped - Check if socket connect or send should be restricted +* based on scope controls. +* +* @other: The server socket. +* @domain: The client domain. +* @scope: The relevant scope bit to check (i.e. pathname or abstract). +* +* Returns: True if connect should be restricted, false otherwise. +*/ static bool sock_is_scoped (struct sock *const other, -const struct landlock_ruleset *const domain) +const struct landlock_ruleset *const domain, +access_mask_t scope) {const struct landlock_ruleset *dom_other;
security/landlock/task.c static bool domain_is_scoped(const struct landlock_ruleset *const client, · line 257
Quoted 10 lines
security/landlock/task.c +2
-3/* The credentials will not change. */ lockdep_assert_held (&unix_sk (other)->lock); dom_other =landlock_cred (other->sk_socket->file->f_cred)->domain; -return domain_is_scoped (domain, dom_other, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +return domain_is_scoped (domain, dom_other, scope); } -static bool is_abstract_socket (struct sock *const sock) +/* Allow us to quickly test if the current domain scopes any form of socket */
Missing final dot.
security/landlock/task.c static bool domain_is_scoped(const struct landlock_ruleset *const client, · line 267
Quoted 10 lines
security/landlock/task.c +10
-0+static const struct access_masks unix_scope = { + .scope =LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET , +}; + +/* +* UNIX sockets can have three types of addresses: pathname (a filesystem path), +* unnamed (not bound to an address), and abstract (sun_path[0] is '\0'). +* Unnamed sockets include those created with socketpair() and unbound sockets. +* We do not restrict unnamed sockets since they have no address to identify.
Not because they have no address but because they cannot be used to reach a new peer, right?
Quoted 8 lines
security/landlock/task.c +6
-1+*/ +static int +check_socket_access (struct sock *const other, +const struct landlock_cred_security *const subject, +const size_t handle_layer) { -struct unix_address *addr =unix_sk (sock)->addr; +const struct unix_address *addr =unix_sk (other)->addr;
security/landlock/task.c static bool domain_is_scoped(const struct landlock_ruleset *const client, · line 283
Quoted 10 lines
security/landlock/task.c +6
-1+access_mask_t scope; +enum landlock_request_type request_type; +/* Unnamed sockets are not restricted. */ if (!addr) -return false; +return 0 ; +/* +* Abstract and pathname Unix sockets have separate scope and audit
UNIX
Quoted 97 lines
security/landlock/task.c +26
-41+* request type. +*/ if (addr->len >= offsetof(struct sockaddr_un , sun_path) +1 && - addr->name->sun_path[0 ] =='\0' ) -return true; + addr->name->sun_path[0 ] =='\0' ) { + scope =LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ; + request_type =LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET ; + }else { + scope =LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ; + request_type =LANDLOCK_REQUEST_SCOPE_PATHNAME_UNIX_SOCKET ; + } -return false; -} +if (!sock_is_scoped (other, subject->domain, scope)) +return 0 ; -static const struct access_masks unix_scope = { - .scope =LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET , -}; +landlock_log_denial (subject, &(struct landlock_request ) { + .type = request_type, + .audit = { + .type =LSM_AUDIT_DATA_NET , + .u.net = &(struct lsm_network_audit ) { + .sk = other, + }, + }, + .layer_plus_one = handle_layer +1 , + }); +return -EPERM ; +} static int hook_unix_stream_connect(struct sock *const sock, struct sock *const other,@@ -275,23 +321,7 @@ static int hook_unix_stream_connect(struct sock *const sock,if (!subject)return 0 ; -if (!is_abstract_socket (other)) -return 0 ; - -if (!sock_is_scoped (other, subject->domain)) -return 0 ; - -landlock_log_denial (subject, &(struct landlock_request ) { - .type =LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET , - .audit = { - .type =LSM_AUDIT_DATA_NET , - .u.net = &(struct lsm_network_audit ) { - .sk = other, - }, - }, - .layer_plus_one = handle_layer +1 , - }); -return -EPERM ; +return check_socket_access (other, subject, handle_layer); } static int hook_unix_may_send(struct socket *const sock,@@ -302,6 +332,7 @@ static int hook_unix_may_send(struct socket *const sock,landlock_get_applicable_subject (current_cred (), unix_scope, &handle_layer); +/* Quick return for non-landlocked tasks. */ if (!subject)return 0 ;@@ -312,23 +343,7 @@ static int hook_unix_may_send(struct socket *const sock,if (unix_peer (sock->sk) == other->sk)return 0 ; -if (!is_abstract_socket (other->sk)) -return 0 ; - -if (!sock_is_scoped (other->sk, subject->domain)) -return 0 ; - -landlock_log_denial (subject, &(struct landlock_request ) { - .type =LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET , - .audit = { - .type =LSM_AUDIT_DATA_NET , - .u.net = &(struct lsm_network_audit ) { - .sk = other->sk, - }, - }, - .layer_plus_one = handle_layer +1 , - }); -return -EPERM ; +return check_socket_access (other->sk, subject, handle_layer); }static const struct access_masks signal_scope = {Signature · 1 line
2.52.0
PATCH v2 3/6 samples/landlock: Support LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
In reply to Tingmao Wang---
I've decided to use "u" as the character to control this scope bit since it stands for (normal) Unix sockets. Imo using "p" or "n" would make it less clear / memorable. Open to suggestions.
Also, open to suggestion whether socket scoping (pathname and abstract) should be enabled by default, if LL_SCOPED is not set. This would break backward compatibility, but maybe we shouldn't guarentee backward compatibility of this sandboxer in the first place, and almost all cases of Landlock usage would want socket scoping.
samples/landlock/sandboxer.c | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-)
samples/landlock/sandboxer.c +18 -5
@@ -234,14 +234,16 @@ static bool check_ruleset_scope(const char *const env_var,bool error = false;bool abstract_scoping = false;bool signal_scoping = false; +bool named_scoping = false;/* Scoping is not supported by Landlock ABI */ if (!(ruleset_attr->scoped & - (LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |LANDLOCK_SCOPE_SIGNAL ))) + (LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |LANDLOCK_SCOPE_SIGNAL | +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ))) goto out_unset; env_type_scope =getenv (env_var); -/* Scoping is not supported by the user */ +/* Scoping is not requested by the user */ if (!env_type_scope ||strcmp ("" , env_type_scope) ==0 ) goto out_unset;
@@ -254,6 +256,9 @@ static bool check_ruleset_scope(const char *const env_var, }else if (strcmp ("s" , ipc_scoping_name) ==0 && !signal_scoping) { signal_scoping = true; + }else if (strcmp ("u" , ipc_scoping_name) ==0 && + !named_scoping) { + named_scoping = true; }else {fprintf (stderr,"Unknown or duplicate scope \"%s\"\n" , ipc_scoping_name);
@@ -270,6 +275,8 @@ static bool check_ruleset_scope(const char *const env_var, ruleset_attr->scoped &= ~LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ;if (!signal_scoping) ruleset_attr->scoped &= ~LANDLOCK_SCOPE_SIGNAL ; +if (!named_scoping) + ruleset_attr->scoped &= ~LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ;unsetenv (env_var);return error;
@@ -299,7 +306,7 @@ static bool check_ruleset_scope(const char *const env_var,/* clang-format on */ -#define LANDLOCK_ABI_LAST 7 +#define LANDLOCK_ABI_LAST 8#define XSTR (s) #s#define STR (s) XSTR(s)
@@ -325,6 +332,7 @@ static const char help[] ="* " ENV_SCOPED_NAME ": actions denied on the outside of the landlock domain\n" " - \"a\" to restrict opening abstract unix sockets\n" " - \"s\" to restrict sending signals\n" +" - \"u\" to restrict opening pathname (non-abstract) unix sockets\n" "\n" "A sandboxer should not log denied access requests to avoid spamming logs, " "but to test audit we can set " ENV_FORCE_LOG_NAME "=1\n"
@@ -334,7 +342,7 @@ static const char help[] =ENV_FS_RW_NAME "=\"/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp\" " ENV_TCP_BIND_NAME "=\"9418\" " ENV_TCP_CONNECT_NAME "=\"80:443\" " -ENV_SCOPED_NAME "=\"a:s\" " +ENV_SCOPED_NAME "=\"a:s:u\" " "%1$s bash -i\n" "\n" "This sandboxer can use Landlock features up to ABI version "
@@ -356,7 +364,8 @@ int main(const int argc, char *const argv[], char *const *const envp) .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | LANDLOCK_ACCESS_NET_CONNECT_TCP, .scoped =LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | - LANDLOCK_SCOPE_SIGNAL, +LANDLOCK_SCOPE_SIGNAL | + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET, };int supported_restrict_flags =LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON ;int set_restrict_flags =0 ;
@@ -436,6 +445,10 @@ int main(const int argc, char *const argv[], char *const *const envp)/* Removes LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON for ABI < 7 */ supported_restrict_flags &= ~LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON ; + __attribute__((fallthrough)); +case 7 : +/* Removes LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET for ABI < 8 */ + ruleset_attr.scoped &= ~LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ;/* Must be printed for any ABI < LANDLOCK_ABI_LAST. */ fprintf(stderr,
Signature · 1 line
2.52.0
Mickaël Salaün
maintainerRe: [PATCH v2 3/6] samples/landlock: Support LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
We should have a (potentially small) description of what this patch does, even if it's a bit redundant with the subject.
Quoted 6 lines · Tingmao Wang
Signed-off-by Tingmao Wang <[email protected]>---
I've decided to use "u" as the character to control this scope bit since it stands for (normal) Unix sockets. Imo using "p" or "n" would make it less clear / memorable. Open to suggestions.
Looks good to me.
Quoted 6 lines
Also, open to suggestion whether socket scoping (pathname and abstract) should be enabled by default, if
LL_SCOPEDis not set. This would break backward compatibility, but maybe we shouldn't guarentee backward compatibility of this sandboxer in the first place, and almost all cases of Landlock usage would want socket scoping.
I agree that this example could have better defaults, but this should be done with a standalone patch series. An important point to keep in mind is that this example is used by developers (e.g. potential copy/paste), so we need to be careful to not encourage them to create code which is backward incompatible. I think the best way to do it is to request a default behavior for a specific Landlock ABI version (e.g. with a new parameter).
I'd also like this example to still be simple to understand, update, and maintain.
Quoted 96 lines
samples/landlock/sandboxer.c | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-)samples/landlock/sandboxer.c +18
-5@@ -234,14 +234,16 @@ static bool check_ruleset_scope(const char *const env_var,bool error = false;bool abstract_scoping = false;bool signal_scoping = false; +bool named_scoping = false;/* Scoping is not supported by Landlock ABI */ if (!(ruleset_attr->scoped & - (LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |LANDLOCK_SCOPE_SIGNAL ))) + (LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |LANDLOCK_SCOPE_SIGNAL | +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ))) goto out_unset; env_type_scope =getenv (env_var); -/* Scoping is not supported by the user */ +/* Scoping is not requested by the user */ if (!env_type_scope ||strcmp ("" , env_type_scope) ==0 ) goto out_unset;@@ -254,6 +256,9 @@ static bool check_ruleset_scope(const char *const env_var, }else if (strcmp ("s" , ipc_scoping_name) ==0 && !signal_scoping) { signal_scoping = true; + }else if (strcmp ("u" , ipc_scoping_name) ==0 && + !named_scoping) { + named_scoping = true; }else {fprintf (stderr,"Unknown or duplicate scope \"%s\"\n" , ipc_scoping_name);@@ -270,6 +275,8 @@ static bool check_ruleset_scope(const char *const env_var, ruleset_attr->scoped &= ~LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ;if (!signal_scoping) ruleset_attr->scoped &= ~LANDLOCK_SCOPE_SIGNAL ; +if (!named_scoping) + ruleset_attr->scoped &= ~LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ;unsetenv (env_var);return error;@@ -299,7 +306,7 @@ static bool check_ruleset_scope(const char *const env_var,/* clang-format on */ -#define LANDLOCK_ABI_LAST 7 +#define LANDLOCK_ABI_LAST 8#define XSTR (s) #s#define STR (s) XSTR(s)@@ -325,6 +332,7 @@ static const char help[] ="* " ENV_SCOPED_NAME ": actions denied on the outside of the landlock domain\n" " - \"a\" to restrict opening abstract unix sockets\n" " - \"s\" to restrict sending signals\n" +" - \"u\" to restrict opening pathname (non-abstract) unix sockets\n" "\n" "A sandboxer should not log denied access requests to avoid spamming logs, " "but to test audit we can set " ENV_FORCE_LOG_NAME "=1\n" @@ -334,7 +342,7 @@ static const char help[] =ENV_FS_RW_NAME "=\"/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp\" " ENV_TCP_BIND_NAME "=\"9418\" " ENV_TCP_CONNECT_NAME "=\"80:443\" " -ENV_SCOPED_NAME "=\"a:s\" " +ENV_SCOPED_NAME "=\"a:s:u\" " "%1$s bash -i\n" "\n" "This sandboxer can use Landlock features up to ABI version " @@ -356,7 +364,8 @@ int main(const int argc, char *const argv[], char *const *const envp) .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | LANDLOCK_ACCESS_NET_CONNECT_TCP, .scoped =LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | - LANDLOCK_SCOPE_SIGNAL, +LANDLOCK_SCOPE_SIGNAL | + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET, };int supported_restrict_flags =LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON ;int set_restrict_flags =0 ;@@ -436,6 +445,10 @@ int main(const int argc, char *const argv[], char *const *const envp)/* Removes LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON for ABI < 7 */ supported_restrict_flags &= ~LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON ; + __attribute__((fallthrough)); +case 7 : +/* Removes LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET for ABI < 8 */ + ruleset_attr.scoped &= ~LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ;/* Must be printed for any ABI < LANDLOCK_ABI_LAST. */ fprintf(stderr,Signature · 1 line
2.52.0
Re: [PATCH v2 3/6] samples/landlock: Support LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
Quoted 21 lines · Mickaël Salaün
We should have a (potentially small) description of what this patch does, even if it's a bit redundant with the subject.
Quoted 6 lines · Tingmao Wang
Signed-off-by Tingmao Wang <[email protected]>---
I've decided to use "u" as the character to control this scope bit since it stands for (normal) Unix sockets. Imo using "p" or "n" would make it less clear / memorable. Open to suggestions.
Looks good to me.
Quoted 6 lines
Also, open to suggestion whether socket scoping (pathname and abstract) should be enabled by default, if
LL_SCOPEDis not set. This would break backward compatibility, but maybe we shouldn't guarentee backward compatibility of this sandboxer in the first place, and almost all cases of Landlock usage would want socket scoping.
Quoted 7 lines
I agree that this example could have better defaults, but this should be done with a standalone patch series. An important point to keep in mind is that this example is used by developers (e.g. potential copy/paste), so we need to be careful to not encourage them to create code which is backward incompatible. I think the best way to do it is to request a default behavior for a specific Landlock ABI version (e.g. with a new parameter).
Just to make sure we're on the same page, I was only talking about whether we keep the behavior of the sandboxer "backward compatible" (i.e. if someone ran a program that relied on accessing UNIX sockets of more privileged programs, if we make the sandboxer start enforcing socket scoping by default, their program would stop working under this sandboxer), I was not suggesting that we do something which will cause the sandboxer itself to no longer work on older kernels.
But on second thought, the sandboxer is already not designed to be relied upon to always behave the same way after an update, since the user don't get to choose which handled access rights are added to the ruleset. With new bits added to either ACCESS_FS_ROUGHLY_READ or ACCESS_FS_ROUGHLY_WRITE, the policy effectively gets more restrictive automatically. For example, once Günther's patch [1] that adds LANDLOCK_ACCESS_FS_RESOLVE_UNIX is merged, the sandboxer will effectively starts restricting pathname UNIX sockets "by default" anyway (under any dirs not listed in LL_FS_RW). So maybe we don't need to think too hard about this.
Mickaël Salaün
maintainerRe: [PATCH v2 3/6] samples/landlock: Support LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
Quoted 30 lines · Tingmao Wang
Quoted 21 lines · Mickaël Salaün
We should have a (potentially small) description of what this patch does, even if it's a bit redundant with the subject.
Quoted 6 lines · Tingmao Wang
Signed-off-by Tingmao Wang <[email protected]>---
I've decided to use "u" as the character to control this scope bit since it stands for (normal) Unix sockets. Imo using "p" or "n" would make it less clear / memorable. Open to suggestions.
Looks good to me.
Quoted 6 lines
Also, open to suggestion whether socket scoping (pathname and abstract) should be enabled by default, if
LL_SCOPEDis not set. This would break backward compatibility, but maybe we shouldn't guarentee backward compatibility of this sandboxer in the first place, and almost all cases of Landlock usage would want socket scoping.Quoted 7 lines
I agree that this example could have better defaults, but this should be done with a standalone patch series. An important point to keep in mind is that this example is used by developers (e.g. potential copy/paste), so we need to be careful to not encourage them to create code which is backward incompatible. I think the best way to do it is to request a default behavior for a specific Landlock ABI version (e.g. with a new parameter).
Quoted 7 lines
Just to make sure we're on the same page, I was only talking about whether we keep the behavior of the sandboxer "backward compatible" (i.e. if someone ran a program that relied on accessing UNIX sockets of more privileged programs, if we make the sandboxer start enforcing socket scoping by default, their program would stop working under this sandboxer), I was not suggesting that we do something which will cause the sandboxer itself to no longer work on older kernels.
Yep, I extrapolated a bit.
Quoted 11 lines
But on second thought, the sandboxer is already not designed to be relied upon to always behave the same way after an update, since the user don't get to choose which handled access rights are added to the ruleset. With new bits added to either
ACCESS_FS_ROUGHLY_READorACCESS_FS_ROUGHLY_WRITE, the policy effectively gets more restrictive automatically. For example, once Günther's patch [1] that addsLANDLOCK_ACCESS_FS_RESOLVE_UNIXis merged, the sandboxer will effectively starts restricting pathname UNIX sockets "by default" anyway (under any dirs not listed inLL_FS_RW). So maybe we don't need to think too hard about this.
Indeed :)
Quoted 3 lines
PATCH v2 4/6 selftests/landlock: Support pathname socket path in set_unix_address
In reply to Tingmao WangTo prepare for extending the socket tests to do non-abstract sockets too, extend set_unix_address() to also be able to populate a non-abstract socket path under TMP_DIR. Also use snprintf for good measure.
This also changes existing callers to pass true for the abstract argument.
--- tools/testing/selftests/landlock/common.h | 33 +++++++++++++++---- tools/testing/selftests/landlock/net_test.c | 2 +- .../landlock/scoped_abstract_unix_test.c | 30 ++++++++--------- .../selftests/landlock/scoped_signal_test.c | 2 +- 4 files changed, 44 insertions(+), 23 deletions(-)
tools/testing/selftests/landlock/common.h +27 -6
@@ -241,13 +241,34 @@ struct service_fixture { }; }; +#define PATHNAME_UNIX_SOCK_DIR TMP_DIR + +/** +* set_unix_address - Set up srv->unix_addr and srv->unix_addr_len. +* @srv: Service fixture containing the socket address to initialize +* @index: Index to include in socket names +* @abstract: If true, creates an abstract socket address (sun_path[0] == +* '\0') with the given name. If false, creates a pathname socket +* address with the given path. +*/ static void __maybe_unused set_unix_address (struct service_fixture *const srv, -const unsigned short index) +const unsigned short index, +const bool abstract) { srv->unix_addr.sun_family =AF_UNIX ; -sprintf (srv->unix_addr.sun_path, -"_selftests-landlock-abstract-unix-tid%d-index%d" ,sys_gettid (), - index); - srv->unix_addr_len =SUN_LEN (&srv->unix_addr); - srv->unix_addr.sun_path[0 ] ='\0' ; +if (abstract) { +snprintf (srv->unix_addr.sun_path, +sizeof (srv->unix_addr.sun_path), +"_selftests-landlock-abstract-unix-tid%d-index%d" , +sys_gettid (), index); + srv->unix_addr_len =SUN_LEN (&srv->unix_addr); + srv->unix_addr.sun_path[0 ] ='\0' ; + }else { +snprintf (srv->unix_addr.sun_path, +sizeof (srv->unix_addr.sun_path), +PATHNAME_UNIX_SOCK_DIR +"/pathname-unix-tid%d-index%d.sock" , +sys_gettid (), index); + srv->unix_addr_len =sizeof (srv->unix_addr); + } }
tools/testing/selftests/landlock/net_test.c +1 -1
@@ -69,7 +69,7 @@ static int set_service(struct service_fixture *const srv,return 0 ;case AF_UNIX : -set_unix_address (srv, index); +set_unix_address (srv, index, true);return 0 ; }return 1 ;
tools/testing/selftests/landlock/scoped_abstract_unix_test.c +15 -15
@@ -58,8 +58,8 @@ FIXTURE_SETUP(scoped_domains)memset (&self->stream_address,0 ,sizeof (self->stream_address));memset (&self->dgram_address,0 ,sizeof (self->dgram_address)); -set_unix_address (&self->stream_address,0 ); -set_unix_address (&self->dgram_address,1 ); +set_unix_address (&self->stream_address,0 , true); +set_unix_address (&self->dgram_address,1 , true); }FIXTURE_TEARDOWN (scoped_domains)
@@ -280,7 +280,7 @@ FIXTURE_SETUP(scoped_audit)disable_caps (_metadata);memset (&self->dgram_address,0 ,sizeof (self->dgram_address)); -set_unix_address (&self->dgram_address,1 ); +set_unix_address (&self->dgram_address,1 , true);set_cap (_metadata,CAP_AUDIT_CONTROL ); self->audit_fd =audit_init_with_exe_filter (&self->audit_filter);
@@ -392,16 +392,16 @@ FIXTURE_SETUP(scoped_vs_unscoped)memset (&self->parent_stream_address,0 ,sizeof (self->parent_stream_address)); -set_unix_address (&self->parent_stream_address,0 ); +set_unix_address (&self->parent_stream_address,0 , true);memset (&self->parent_dgram_address,0 ,sizeof (self->parent_dgram_address)); -set_unix_address (&self->parent_dgram_address,1 ); +set_unix_address (&self->parent_dgram_address,1 , true);memset (&self->child_stream_address,0 ,sizeof (self->child_stream_address)); -set_unix_address (&self->child_stream_address,2 ); +set_unix_address (&self->child_stream_address,2 , true);memset (&self->child_dgram_address,0 ,sizeof (self->child_dgram_address)); -set_unix_address (&self->child_dgram_address,3 ); +set_unix_address (&self->child_dgram_address,3 , true); }FIXTURE_TEARDOWN (scoped_vs_unscoped)
@@ -622,9 +622,9 @@ FIXTURE_SETUP(outside_socket)drop_caps (_metadata);memset (&self->transit_address,0 ,sizeof (self->transit_address)); -set_unix_address (&self->transit_address,0 ); +set_unix_address (&self->transit_address,0 , true);memset (&self->address,0 ,sizeof (self->address)); -set_unix_address (&self->address,1 ); +set_unix_address (&self->address,1 , true); }FIXTURE_TEARDOWN (outside_socket)
@@ -802,9 +802,9 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)/* Abstract address. */ memset (&stream_abstract_addr,0 ,sizeof (stream_abstract_addr)); -set_unix_address (&stream_abstract_addr,0 ); +set_unix_address (&stream_abstract_addr,0 , true);memset (&dgram_abstract_addr,0 ,sizeof (dgram_abstract_addr)); -set_unix_address (&dgram_abstract_addr,1 ); +set_unix_address (&dgram_abstract_addr,1 , true);/* Unnamed address for datagram socket. */ ASSERT_EQ (0 ,socketpair (AF_UNIX ,SOCK_DGRAM ,0 , unnamed_sockets));
@@ -990,9 +990,9 @@ TEST(datagram_sockets)drop_caps (_metadata);memset (&connected_addr,0 ,sizeof (connected_addr)); -set_unix_address (&connected_addr,0 ); +set_unix_address (&connected_addr,0 , true);memset (&non_connected_addr,0 ,sizeof (non_connected_addr)); -set_unix_address (&non_connected_addr,1 ); +set_unix_address (&non_connected_addr,1 , true);ASSERT_EQ (0 ,pipe2 (pipe_parent,O_CLOEXEC ));ASSERT_EQ (0 ,pipe2 (pipe_child,O_CLOEXEC ));
@@ -1090,9 +1090,9 @@ TEST(self_connect)drop_caps (_metadata);memset (&connected_addr,0 ,sizeof (connected_addr)); -set_unix_address (&connected_addr,0 ); +set_unix_address (&connected_addr,0 , true);memset (&non_connected_addr,0 ,sizeof (non_connected_addr)); -set_unix_address (&non_connected_addr,1 ); +set_unix_address (&non_connected_addr,1 , true); connected_socket =socket (AF_UNIX ,SOCK_DGRAM ,0 ); non_connected_socket =socket (AF_UNIX ,SOCK_DGRAM ,0 );
tools/testing/selftests/landlock/scoped_signal_test.c +1 -1
@@ -463,7 +463,7 @@ TEST_F(fown, sigurg_socket)pid_t child;memset (&server_address,0 ,sizeof (server_address)); -set_unix_address (&server_address,0 ); +set_unix_address (&server_address,0 , true);ASSERT_EQ (0 ,pipe2 (pipe_parent,O_CLOEXEC ));ASSERT_EQ (0 ,pipe2 (pipe_child,O_CLOEXEC ));
Signature · 1 line
2.52.0
PATCH v2 5/6 selftests/landlock: Repurpose scoped_abstract_unix_test.c for pathname sockets too.
In reply to Tingmao WangSince there is very little difference between abstract and pathname sockets in terms of testing of the scoped access checks (the only difference is in which scope bit control which form of socket), it makes sense to reuse the existing test for both type of sockets. Therefore, we rename scoped_abstract_unix_test.c to scoped_unix_test.c and extend the scoped_domains test to test pathname (i.e. non-abstract) sockets too.
Since we can't change the variant data of scoped_domains (as it is defined in the shared .h file), we do this by extracting the actual test code into a function, and call it from different test cases.
Also extend scoped_audit (this time we can use variants) to test both abstract and pathname sockets. For pathname sockets, audit_log_lsm_data will produce path="..." (or hex if path contains control characters) with absolute paths from the dentry, so we need to construct the escaped regex for the real path like in fs_test.
---
...bstract_unix_test.c => scoped_unix_test.c} | 256 ++++++++++++++----
1 file changed, 206 insertions(+), 50 deletions(-)
rename tools/testing/selftests/landlock/{scoped_abstract_unix_test.c => scoped_unix_test.c} (81%)tools/testing/selftests/landlock/scoped_unix_test.c renamed from tools/testing/selftests/landlock/scoped_abstract_unix_test.c +206 -50
@@ -1,6 +1,7 @@// SPDX-License-Identifier: GPL-2.0 /* -* Landlock tests - Abstract UNIX socket +* Landlock tests - Scoped access checks for UNIX socket (abstract and +* pathname) * * Copyright © 2024 Tahera Fahimi <[email protected]> */
@@ -19,6 +20,7 @@#include <sys/un.h> #include <sys/wait.h> #include <unistd.h> +#include <stdlib.h> #include "audit.h" #include "common.h"
@@ -47,7 +49,8 @@ static void create_fs_domain(struct __test_metadata *const _metadata)FIXTURE (scoped_domains) { -struct service_fixture stream_address, dgram_address; +struct service_fixture stream_address_abstract, dgram_address_abstract, + stream_address_pathname, dgram_address_pathname; };#include "scoped_base_variants.h"
@@ -56,27 +59,62 @@ FIXTURE_SETUP(scoped_domains) {drop_caps (_metadata); -memset (&self->stream_address,0 ,sizeof (self->stream_address)); -memset (&self->dgram_address,0 ,sizeof (self->dgram_address)); -set_unix_address (&self->stream_address,0 , true); -set_unix_address (&self->dgram_address,1 , true); +ASSERT_EQ (0 ,mkdir (PATHNAME_UNIX_SOCK_DIR ,0700 )); + +memset (&self->stream_address_abstract,0 , +sizeof (self->stream_address_abstract)); +memset (&self->dgram_address_abstract,0 , +sizeof (self->dgram_address_abstract)); +memset (&self->stream_address_pathname,0 , +sizeof (self->stream_address_pathname)); +memset (&self->dgram_address_pathname,0 , +sizeof (self->dgram_address_pathname)); +set_unix_address (&self->stream_address_abstract,0 , true); +set_unix_address (&self->dgram_address_abstract,1 , true); +set_unix_address (&self->stream_address_pathname,0 , false); +set_unix_address (&self->dgram_address_pathname,1 , false); +} + +/* Remove @path if it exists */ +int remove_path (const char *path) +{ +if (unlink (path) ==-1 ) { +if (errno !=ENOENT ) +return -errno; + } +return 0 ; }FIXTURE_TEARDOWN (scoped_domains) { +EXPECT_EQ (0 ,remove_path (self->stream_address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,remove_path (self->dgram_address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,rmdir (PATHNAME_UNIX_SOCK_DIR )); }/* * Test unix_stream_connect() and unix_may_send() for a child connecting to its * parent, when they have scoped domain or no domain. */ -TEST_F(scoped_domains, connect_to_parent) +static void test_connect_to_parent (struct __test_metadata *const _metadata, +FIXTURE_DATA (scoped_domains) * self, +const FIXTURE_VARIANT (scoped_domains) * + variant, +const bool abstract) {pid_t child;bool can_connect_to_parent;int status;int pipe_parent[2 ];int stream_server, dgram_server; +const __u16 scope = abstract ?LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET : +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ; +const struct service_fixture *stream_address = + abstract ? &self->stream_address_abstract : + &self->stream_address_pathname; +const struct service_fixture *dgram_address = + abstract ? &self->dgram_address_abstract : + &self->dgram_address_pathname;/* * can_connect_to_parent is true if a child process can connect to its
@@ -87,8 +125,7 @@ TEST_F(scoped_domains, connect_to_parent)ASSERT_EQ (0 ,pipe2 (pipe_parent,O_CLOEXEC ));if (variant->domain_both) { -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope);if (!__test_passed (_metadata))return ; }
@@ -102,8 +139,7 @@ TEST_F(scoped_domains, connect_to_parent)EXPECT_EQ (0 ,close (pipe_parent[1 ]));if (variant->domain_child) -create_scoped_domain ( - _metadata,LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope); stream_client =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_client);
@@ -113,8 +149,8 @@ TEST_F(scoped_domains, connect_to_parent)/* Waits for the server. */ ASSERT_EQ (1 ,read (pipe_parent[0 ], &buf_child,1 )); - err =connect (stream_client, &self->stream_address.unix_addr, - self->stream_address.unix_addr_len); + err =connect (stream_client, &stream_address->unix_addr, + stream_address->unix_addr_len);if (can_connect_to_parent) {EXPECT_EQ (0 , err); }else {
@@ -123,8 +159,8 @@ TEST_F(scoped_domains, connect_to_parent) }EXPECT_EQ (0 ,close (stream_client)); - err =connect (dgram_client, &self->dgram_address.unix_addr, - self->dgram_address.unix_addr_len); + err =connect (dgram_client, &dgram_address->unix_addr, + dgram_address->unix_addr_len);if (can_connect_to_parent) {EXPECT_EQ (0 , err); }else {
@@ -137,17 +173,16 @@ TEST_F(scoped_domains, connect_to_parent) }EXPECT_EQ (0 ,close (pipe_parent[0 ]));if (variant->domain_parent) -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope); stream_server =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_server); dgram_server =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , dgram_server); -ASSERT_EQ (0 ,bind (stream_server, &self->stream_address.unix_addr, - self->stream_address.unix_addr_len)); -ASSERT_EQ (0 ,bind (dgram_server, &self->dgram_address.unix_addr, - self->dgram_address.unix_addr_len)); +ASSERT_EQ (0 ,bind (stream_server, &stream_address->unix_addr, + stream_address->unix_addr_len)); +ASSERT_EQ (0 ,bind (dgram_server, &dgram_address->unix_addr, + dgram_address->unix_addr_len));ASSERT_EQ (0 ,listen (stream_server, backlog));/* Signals to child that the parent is listening. */
@@ -166,7 +201,11 @@ TEST_F(scoped_domains, connect_to_parent)* Test unix_stream_connect() and unix_may_send() for a parent connecting to * its child, when they have scoped domain or no domain. */ -TEST_F(scoped_domains, connect_to_child) +static void test_connect_to_child (struct __test_metadata *const _metadata, +FIXTURE_DATA (scoped_domains) * self, +const FIXTURE_VARIANT (scoped_domains) * + variant, +const bool abstract) {pid_t child;bool can_connect_to_child;
@@ -174,6 +213,14 @@ TEST_F(scoped_domains, connect_to_child)int pipe_child[2 ], pipe_parent[2 ];char buf;int stream_client, dgram_client; +const __u16 scope = abstract ?LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET : +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ; +const struct service_fixture *stream_address = + abstract ? &self->stream_address_abstract : + &self->stream_address_pathname; +const struct service_fixture *dgram_address = + abstract ? &self->dgram_address_abstract : + &self->dgram_address_pathname;/* * can_connect_to_child is true if a parent process can connect to its
@@ -185,8 +232,7 @@ TEST_F(scoped_domains, connect_to_child)ASSERT_EQ (0 ,pipe2 (pipe_child,O_CLOEXEC ));ASSERT_EQ (0 ,pipe2 (pipe_parent,O_CLOEXEC ));if (variant->domain_both) { -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope);if (!__test_passed (_metadata))return ; }
@@ -199,8 +245,7 @@ TEST_F(scoped_domains, connect_to_child)EXPECT_EQ (0 ,close (pipe_parent[1 ]));EXPECT_EQ (0 ,close (pipe_child[0 ]));if (variant->domain_child) -create_scoped_domain ( - _metadata,LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope);/* Waits for the parent to be in a domain, if any. */ ASSERT_EQ (1 ,read (pipe_parent[0 ], &buf,1 ));
@@ -209,11 +254,10 @@ TEST_F(scoped_domains, connect_to_child)ASSERT_LE (0 , stream_server); dgram_server =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , dgram_server); -ASSERT_EQ (0 , -bind (stream_server, &self->stream_address.unix_addr, - self->stream_address.unix_addr_len)); -ASSERT_EQ (0 ,bind (dgram_server, &self->dgram_address.unix_addr, - self->dgram_address.unix_addr_len)); +ASSERT_EQ (0 ,bind (stream_server, &stream_address->unix_addr, + stream_address->unix_addr_len)); +ASSERT_EQ (0 ,bind (dgram_server, &dgram_address->unix_addr, + dgram_address->unix_addr_len));ASSERT_EQ (0 ,listen (stream_server, backlog));/* Signals to the parent that child is listening. */
@@ -230,8 +274,7 @@ TEST_F(scoped_domains, connect_to_child)EXPECT_EQ (0 ,close (pipe_parent[0 ]));if (variant->domain_parent) -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope);/* Signals that the parent is in a domain, if any. */ ASSERT_EQ (1 ,write (pipe_parent[1 ],"." ,1 ));
@@ -243,11 +286,11 @@ TEST_F(scoped_domains, connect_to_child)/* Waits for the child to listen */ ASSERT_EQ (1 ,read (pipe_child[0 ], &buf,1 )); - err_stream =connect (stream_client, &self->stream_address.unix_addr, - self->stream_address.unix_addr_len); + err_stream =connect (stream_client, &stream_address->unix_addr, + stream_address->unix_addr_len); errno_stream = errno; - err_dgram =connect (dgram_client, &self->dgram_address.unix_addr, - self->dgram_address.unix_addr_len); + err_dgram =connect (dgram_client, &dgram_address->unix_addr, + dgram_address->unix_addr_len); errno_dgram = errno;if (can_connect_to_child) {EXPECT_EQ (0 , err_stream);
@@ -268,19 +311,79 @@ TEST_F(scoped_domains, connect_to_child) _metadata->exit_code =KSFT_FAIL ; } +/* +* Test unix_stream_connect() and unix_may_send() for a child connecting to its +* parent, when they have scoped domain or no domain. +*/ +TEST_F(scoped_domains, abstract_connect_to_parent) +{ +test_connect_to_parent (_metadata, self, variant, true); +} + +/* +* Test unix_stream_connect() and unix_may_send() for a parent connecting to +* its child, when they have scoped domain or no domain. +*/ +TEST_F(scoped_domains, abstract_connect_to_child) +{ +test_connect_to_child (_metadata, self, variant, true); +} + +/* +* Test unix_stream_connect() and unix_may_send() for a child connecting to its +* parent with pathname sockets. +*/ +TEST_F(scoped_domains, pathname_connect_to_parent) +{ +test_connect_to_parent (_metadata, self, variant, false); +} + +/* +* Test unix_stream_connect() and unix_may_send() for a parent connecting to +* its child with pathname sockets. +*/ +TEST_F(scoped_domains, pathname_connect_to_child) +{ +test_connect_to_child (_metadata, self, variant, false); +} +FIXTURE (scoped_audit) { -struct service_fixture dgram_address; +struct service_fixture dgram_address_abstract, dgram_address_pathname;struct audit_filter audit_filter;int audit_fd; }; +FIXTURE_VARIANT (scoped_audit) +{ +const bool abstract_socket; +}; + +// clang-format off +FIXTURE_VARIANT_ADD (scoped_audit, abstract_socket) +{ +// clang-format on + .abstract_socket = true, +}; + +// clang-format off +FIXTURE_VARIANT_ADD (scoped_audit, pathname_socket) +{ +// clang-format on + .abstract_socket = false, +}; +FIXTURE_SETUP (scoped_audit) {disable_caps (_metadata); -memset (&self->dgram_address,0 ,sizeof (self->dgram_address)); -set_unix_address (&self->dgram_address,1 , true); +ASSERT_EQ (0 ,mkdir (PATHNAME_UNIX_SOCK_DIR ,0700 )); +memset (&self->dgram_address_abstract,0 , +sizeof (self->dgram_address_abstract)); +memset (&self->dgram_address_pathname,0 , +sizeof (self->dgram_address_pathname)); +set_unix_address (&self->dgram_address_abstract,1 , true); +set_unix_address (&self->dgram_address_pathname,1 , false);set_cap (_metadata,CAP_AUDIT_CONTROL ); self->audit_fd =audit_init_with_exe_filter (&self->audit_filter);
@@ -291,6 +394,8 @@ FIXTURE_SETUP(scoped_audit)FIXTURE_TEARDOWN_PARENT (scoped_audit) {EXPECT_EQ (0 ,audit_cleanup (-1 ,NULL )); +EXPECT_EQ (0 ,remove_path (self->dgram_address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,rmdir (PATHNAME_UNIX_SOCK_DIR )); }/* python -c 'print(b"\0selftests-landlock-abstract-unix-".hex().upper())' */
@@ -308,6 +413,12 @@ TEST_F(scoped_audit, connect_to_child)char buf;int dgram_client;struct audit_records records; +struct service_fixture *const dgram_address = + variant->abstract_socket ? &self->dgram_address_abstract : + &self->dgram_address_pathname; +size_t log_match_remaining =500 ; +char log_match[log_match_remaining]; +char *log_match_cursor = log_match;/* Makes sure there is no superfluous logged records. */ EXPECT_EQ (0 ,audit_count_records (self->audit_fd, &records));
@@ -330,8 +441,8 @@ TEST_F(scoped_audit, connect_to_child) dgram_server =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , dgram_server); -ASSERT_EQ (0 ,bind (dgram_server, &self->dgram_address.unix_addr, - self->dgram_address.unix_addr_len)); +ASSERT_EQ (0 ,bind (dgram_server, &dgram_address->unix_addr, + dgram_address->unix_addr_len));/* Signals to the parent that child is listening. */ ASSERT_EQ (1 ,write (pipe_child[1 ],"." ,1 ));
@@ -345,7 +456,9 @@ TEST_F(scoped_audit, connect_to_child)EXPECT_EQ (0 ,close (pipe_child[1 ]));EXPECT_EQ (0 ,close (pipe_parent[0 ])); -create_scoped_domain (_metadata,LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, +LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET );/* Signals that the parent is in a domain, if any. */ ASSERT_EQ (1 ,write (pipe_parent[1 ],"." ,1 ));
@@ -355,19 +468,62 @@ TEST_F(scoped_audit, connect_to_child)/* Waits for the child to listen */ ASSERT_EQ (1 ,read (pipe_child[0 ], &buf,1 )); - err_dgram =connect (dgram_client, &self->dgram_address.unix_addr, - self->dgram_address.unix_addr_len); + err_dgram =connect (dgram_client, &dgram_address->unix_addr, + dgram_address->unix_addr_len);EXPECT_EQ (-1 , err_dgram);EXPECT_EQ (EPERM , errno); -EXPECT_EQ ( -0 , -audit_match_record ( - self->audit_fd,AUDIT_LANDLOCK_ACCESS , +if (variant->abstract_socket) { + log_match_cursor =stpncpy ( + log_match,REGEX_LANDLOCK_PREFIX " blockers=scope\\.abstract_unix_socket path=" ABSTRACT_SOCKET_PATH_PREFIX "[0-9A-F]\\+$" , -NULL )); + log_match_remaining); + log_match_remaining = +sizeof (log_match) - (log_match_cursor - log_match); +ASSERT_NE (0 , log_match_remaining); + }else { +/* +* It is assumed that absolute_path does not contain control +* characters nor spaces, see audit_string_contains_control(). +*/ +char *absolute_path = +realpath (dgram_address->unix_addr.sun_path,NULL ); + + EXPECT_NE(NULL, absolute_path) + { +TH_LOG ("realpath() failed: %s" ,strerror (errno)); +return ; + } + + log_match_cursor = +stpncpy (log_match, +REGEX_LANDLOCK_PREFIX +" blockers=scope\\.pathname_unix_socket path=\"" , + log_match_remaining); + log_match_remaining = +sizeof (log_match) - (log_match_cursor - log_match); +ASSERT_NE (0 , log_match_remaining); + log_match_cursor =regex_escape (absolute_path, log_match_cursor, + log_match_remaining); +free (absolute_path); +if (log_match_cursor <0 ) { +TH_LOG ("regex_escape() failed (buffer too small)" ); +return ; + } + log_match_remaining = +sizeof (log_match) - (log_match_cursor - log_match); +ASSERT_NE (0 , log_match_remaining); + log_match_cursor = +stpncpy (log_match_cursor,"\"$" , log_match_remaining); + log_match_remaining = +sizeof (log_match) - (log_match_cursor - log_match); +ASSERT_NE (0 , log_match_remaining); + } + +EXPECT_EQ (0 ,audit_match_record (self->audit_fd,AUDIT_LANDLOCK_ACCESS , + log_match,NULL ));ASSERT_EQ (1 ,write (pipe_parent[1 ],"." ,1 ));EXPECT_EQ (0 ,close (dgram_client));
Signature · 1 line
2.52.0
Mickaël Salaün
maintainerRe: [PATCH v2 5/6] selftests/landlock: Repurpose scoped_abstract_unix_test.c for pathname sockets too.
Commit messages should fit in 72 columns. The subject can be a bit more but we should avoid that, and it should not end with a dot.
Quoted 324 lines · Tingmao Wang
Since there is very little difference between abstract and pathname sockets in terms of testing of the scoped access checks (the only difference is in which scope bit control which form of socket), it makes sense to reuse the existing test for both type of sockets. Therefore, we rename
scoped_abstract_unix_test.ctoscoped_unix_test.cand extend thescoped_domainstest to test pathname (i.e. non-abstract) sockets too.Since we can't change the variant data of
scoped_domains(as it is defined in the shared .h file), we do this by extracting the actual test code into a function, and call it from different test cases.Also extend
scoped_audit(this time we can use variants) to test both abstract and pathname sockets. For pathname sockets,audit_log_lsm_datawill produce path="..." (or hex if path contains control characters) with absolute paths from the dentry, so we need to construct the escaped regex for the real path like infs_test.Signed-off-by Tingmao Wang <[email protected]>--- ...bstract_unix_test.c => scoped_unix_test.c} | 256 ++++++++++++++---- 1 file changed, 206 insertions(+), 50 deletions(-) rename tools/testing/selftests/landlock/{scoped_abstract_unix_test.c => scoped_unix_test.c} (81%)tools/testing/selftests/landlock/scoped_unix_test.c renamed from tools/testing/selftests/landlock/scoped_abstract_unix_test.c +117
-38@@ -1,6 +1,7 @@// SPDX-License-Identifier: GPL-2.0 /* -* Landlock tests - Abstract UNIX socket +* Landlock tests - Scoped access checks for UNIX socket (abstract and +* pathname) * * Copyright © 2024 Tahera Fahimi <[email protected]> */ @@ -19,6 +20,7 @@#include <sys/un.h> #include <sys/wait.h> #include <unistd.h> +#include <stdlib.h> #include "audit.h" #include "common.h" @@ -47,7 +49,8 @@ static void create_fs_domain(struct __test_metadata *const _metadata)FIXTURE (scoped_domains) { -struct service_fixture stream_address, dgram_address; +struct service_fixture stream_address_abstract, dgram_address_abstract, + stream_address_pathname, dgram_address_pathname; };#include "scoped_base_variants.h" @@ -56,27 +59,62 @@ FIXTURE_SETUP(scoped_domains) {drop_caps (_metadata); -memset (&self->stream_address,0 ,sizeof (self->stream_address)); -memset (&self->dgram_address,0 ,sizeof (self->dgram_address)); -set_unix_address (&self->stream_address,0 , true); -set_unix_address (&self->dgram_address,1 , true); +ASSERT_EQ (0 ,mkdir (PATHNAME_UNIX_SOCK_DIR ,0700 )); + +memset (&self->stream_address_abstract,0 , +sizeof (self->stream_address_abstract)); +memset (&self->dgram_address_abstract,0 , +sizeof (self->dgram_address_abstract)); +memset (&self->stream_address_pathname,0 , +sizeof (self->stream_address_pathname)); +memset (&self->dgram_address_pathname,0 , +sizeof (self->dgram_address_pathname)); +set_unix_address (&self->stream_address_abstract,0 , true); +set_unix_address (&self->dgram_address_abstract,1 , true); +set_unix_address (&self->stream_address_pathname,0 , false); +set_unix_address (&self->dgram_address_pathname,1 , false); +} + +/* Remove @path if it exists */ +int remove_path (const char *path) +{ +if (unlink (path) ==-1 ) { +if (errno !=ENOENT ) +return -errno; + } +return 0 ; }FIXTURE_TEARDOWN (scoped_domains) { +EXPECT_EQ (0 ,remove_path (self->stream_address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,remove_path (self->dgram_address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,rmdir (PATHNAME_UNIX_SOCK_DIR )); }/* * Test unix_stream_connect() and unix_may_send() for a child connecting to its * parent, when they have scoped domain or no domain. */ -TEST_F(scoped_domains, connect_to_parent) +static void test_connect_to_parent (struct __test_metadata *const _metadata, +FIXTURE_DATA (scoped_domains) * self, +const FIXTURE_VARIANT (scoped_domains) * + variant, +const bool abstract) {pid_t child;bool can_connect_to_parent;int status;int pipe_parent[2 ];int stream_server, dgram_server; +const __u16 scope = abstract ?LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET : +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ; +const struct service_fixture *stream_address = + abstract ? &self->stream_address_abstract : + &self->stream_address_pathname; +const struct service_fixture *dgram_address = + abstract ? &self->dgram_address_abstract : + &self->dgram_address_pathname;/* * can_connect_to_parent is true if a child process can connect to its @@ -87,8 +125,7 @@ TEST_F(scoped_domains, connect_to_parent)ASSERT_EQ (0 ,pipe2 (pipe_parent,O_CLOEXEC ));if (variant->domain_both) { -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope);if (!__test_passed (_metadata))return ; }@@ -102,8 +139,7 @@ TEST_F(scoped_domains, connect_to_parent)EXPECT_EQ (0 ,close (pipe_parent[1 ]));if (variant->domain_child) -create_scoped_domain ( - _metadata,LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope); stream_client =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_client);@@ -113,8 +149,8 @@ TEST_F(scoped_domains, connect_to_parent)/* Waits for the server. */ ASSERT_EQ (1 ,read (pipe_parent[0 ], &buf_child,1 )); - err =connect (stream_client, &self->stream_address.unix_addr, - self->stream_address.unix_addr_len); + err =connect (stream_client, &stream_address->unix_addr, + stream_address->unix_addr_len);if (can_connect_to_parent) {EXPECT_EQ (0 , err); }else {@@ -123,8 +159,8 @@ TEST_F(scoped_domains, connect_to_parent) }EXPECT_EQ (0 ,close (stream_client)); - err =connect (dgram_client, &self->dgram_address.unix_addr, - self->dgram_address.unix_addr_len); + err =connect (dgram_client, &dgram_address->unix_addr, + dgram_address->unix_addr_len);if (can_connect_to_parent) {EXPECT_EQ (0 , err); }else {@@ -137,17 +173,16 @@ TEST_F(scoped_domains, connect_to_parent) }EXPECT_EQ (0 ,close (pipe_parent[0 ]));if (variant->domain_parent) -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope); stream_server =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_server); dgram_server =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , dgram_server); -ASSERT_EQ (0 ,bind (stream_server, &self->stream_address.unix_addr, - self->stream_address.unix_addr_len)); -ASSERT_EQ (0 ,bind (dgram_server, &self->dgram_address.unix_addr, - self->dgram_address.unix_addr_len)); +ASSERT_EQ (0 ,bind (stream_server, &stream_address->unix_addr, + stream_address->unix_addr_len)); +ASSERT_EQ (0 ,bind (dgram_server, &dgram_address->unix_addr, + dgram_address->unix_addr_len));ASSERT_EQ (0 ,listen (stream_server, backlog));/* Signals to child that the parent is listening. */ @@ -166,7 +201,11 @@ TEST_F(scoped_domains, connect_to_parent)* Test unix_stream_connect() and unix_may_send() for a parent connecting to * its child, when they have scoped domain or no domain. */ -TEST_F(scoped_domains, connect_to_child) +static void test_connect_to_child (struct __test_metadata *const _metadata, +FIXTURE_DATA (scoped_domains) * self, +const FIXTURE_VARIANT (scoped_domains) * + variant, +const bool abstract) {pid_t child;bool can_connect_to_child;@@ -174,6 +213,14 @@ TEST_F(scoped_domains, connect_to_child)int pipe_child[2 ], pipe_parent[2 ];char buf;int stream_client, dgram_client; +const __u16 scope = abstract ?LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET : +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ; +const struct service_fixture *stream_address = + abstract ? &self->stream_address_abstract : + &self->stream_address_pathname; +const struct service_fixture *dgram_address = + abstract ? &self->dgram_address_abstract : + &self->dgram_address_pathname;/* * can_connect_to_child is true if a parent process can connect to its @@ -185,8 +232,7 @@ TEST_F(scoped_domains, connect_to_child)ASSERT_EQ (0 ,pipe2 (pipe_child,O_CLOEXEC ));ASSERT_EQ (0 ,pipe2 (pipe_parent,O_CLOEXEC ));if (variant->domain_both) { -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope);if (!__test_passed (_metadata))return ; }@@ -199,8 +245,7 @@ TEST_F(scoped_domains, connect_to_child)EXPECT_EQ (0 ,close (pipe_parent[1 ]));EXPECT_EQ (0 ,close (pipe_child[0 ]));if (variant->domain_child) -create_scoped_domain ( - _metadata,LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope);/* Waits for the parent to be in a domain, if any. */ ASSERT_EQ (1 ,read (pipe_parent[0 ], &buf,1 ));@@ -209,11 +254,10 @@ TEST_F(scoped_domains, connect_to_child)ASSERT_LE (0 , stream_server); dgram_server =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , dgram_server); -ASSERT_EQ (0 , -bind (stream_server, &self->stream_address.unix_addr, - self->stream_address.unix_addr_len)); -ASSERT_EQ (0 ,bind (dgram_server, &self->dgram_address.unix_addr, - self->dgram_address.unix_addr_len)); +ASSERT_EQ (0 ,bind (stream_server, &stream_address->unix_addr, + stream_address->unix_addr_len)); +ASSERT_EQ (0 ,bind (dgram_server, &dgram_address->unix_addr, + dgram_address->unix_addr_len));ASSERT_EQ (0 ,listen (stream_server, backlog));/* Signals to the parent that child is listening. */ @@ -230,8 +274,7 @@ TEST_F(scoped_domains, connect_to_child)EXPECT_EQ (0 ,close (pipe_parent[0 ]));if (variant->domain_parent) -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope);/* Signals that the parent is in a domain, if any. */ ASSERT_EQ (1 ,write (pipe_parent[1 ],"." ,1 ));@@ -243,11 +286,11 @@ TEST_F(scoped_domains, connect_to_child)/* Waits for the child to listen */ ASSERT_EQ (1 ,read (pipe_child[0 ], &buf,1 )); - err_stream =connect (stream_client, &self->stream_address.unix_addr, - self->stream_address.unix_addr_len); + err_stream =connect (stream_client, &stream_address->unix_addr, + stream_address->unix_addr_len); errno_stream = errno; - err_dgram =connect (dgram_client, &self->dgram_address.unix_addr, - self->dgram_address.unix_addr_len); + err_dgram =connect (dgram_client, &dgram_address->unix_addr, + dgram_address->unix_addr_len); errno_dgram = errno;if (can_connect_to_child) {EXPECT_EQ (0 , err_stream);@@ -268,19 +311,79 @@ TEST_F(scoped_domains, connect_to_child) _metadata->exit_code =KSFT_FAIL ; } +/* +* Test unix_stream_connect() and unix_may_send() for a child connecting to its +* parent, when they have scoped domain or no domain. +*/ +TEST_F(scoped_domains, abstract_connect_to_parent) +{ +test_connect_to_parent (_metadata, self, variant, true); +} + +/* +* Test unix_stream_connect() and unix_may_send() for a parent connecting to +* its child, when they have scoped domain or no domain. +*/ +TEST_F(scoped_domains, abstract_connect_to_child) +{ +test_connect_to_child (_metadata, self, variant, true); +} + +/* +* Test unix_stream_connect() and unix_may_send() for a child connecting to its +* parent with pathname sockets. +*/ +TEST_F(scoped_domains, pathname_connect_to_parent) +{ +test_connect_to_parent (_metadata, self, variant, false); +} + +/* +* Test unix_stream_connect() and unix_may_send() for a parent connecting to +* its child with pathname sockets. +*/ +TEST_F(scoped_domains, pathname_connect_to_child) +{ +test_connect_to_child (_metadata, self, variant, false); +} +FIXTURE (scoped_audit) { -struct service_fixture dgram_address; +struct service_fixture dgram_address_abstract, dgram_address_pathname;
tools/testing/selftests/landlock/scoped_unix_test.c TEST_F(scoped_domains, connect_to_child) · line 362
Quoted 10 lines
tools/testing/selftests/landlock/scoped_unix_test.c renamed from tools/testing/selftests/landlock/scoped_abstract_unix_test.c +6
-0struct audit_filter audit_filter;int audit_fd; }; +FIXTURE_VARIANT (scoped_audit) +{ +const bool abstract_socket; +}; + +// clang-format off
We always use /* */ comments. Ditto for all clang-format markups.
Quoted 39 lines
tools/testing/selftests/landlock/scoped_unix_test.c renamed from tools/testing/selftests/landlock/scoped_abstract_unix_test.c +22
-2+FIXTURE_VARIANT_ADD (scoped_audit, abstract_socket) +{ +// clang-format on + .abstract_socket = true, +}; + +// clang-format off +FIXTURE_VARIANT_ADD (scoped_audit, pathname_socket) +{ +// clang-format on + .abstract_socket = false, +}; +FIXTURE_SETUP (scoped_audit) {disable_caps (_metadata); -memset (&self->dgram_address,0 ,sizeof (self->dgram_address)); -set_unix_address (&self->dgram_address,1 , true); +ASSERT_EQ (0 ,mkdir (PATHNAME_UNIX_SOCK_DIR ,0700 )); +memset (&self->dgram_address_abstract,0 , +sizeof (self->dgram_address_abstract)); +memset (&self->dgram_address_pathname,0 , +sizeof (self->dgram_address_pathname)); +set_unix_address (&self->dgram_address_abstract,1 , true); +set_unix_address (&self->dgram_address_pathname,1 , false);set_cap (_metadata,CAP_AUDIT_CONTROL ); self->audit_fd =audit_init_with_exe_filter (&self->audit_filter);@@ -291,6 +394,8 @@ FIXTURE_SETUP(scoped_audit)FIXTURE_TEARDOWN_PARENT (scoped_audit) {EXPECT_EQ (0 ,audit_cleanup (-1 ,NULL )); +EXPECT_EQ (0 ,remove_path (self->dgram_address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,rmdir (PATHNAME_UNIX_SOCK_DIR )); }/* python -c 'print(b"\0selftests-landlock-abstract-unix-".hex().upper())' */
tools/testing/selftests/landlock/scoped_unix_test.c TEST_F(scoped_audit, connect_to_child) · line 419
Quoted 7 lines
tools/testing/selftests/landlock/scoped_unix_test.c renamed from tools/testing/selftests/landlock/scoped_abstract_unix_test.c +4
-0@@ -308,6 +413,12 @@ TEST_F(scoped_audit, connect_to_child)char buf;int dgram_client;struct audit_records records; +struct service_fixture *const dgram_address = + variant->abstract_socket ? &self->dgram_address_abstract : + &self->dgram_address_pathname; +size_t log_match_remaining =500 ;
const
Why this number? Could you please follow the same logic as in matches_log_fs_extra()?
Quoted 49 lines
tools/testing/selftests/landlock/scoped_unix_test.c renamed from tools/testing/selftests/landlock/scoped_abstract_unix_test.c +12
-10+char log_match[log_match_remaining]; +char *log_match_cursor = log_match;/* Makes sure there is no superfluous logged records. */ EXPECT_EQ (0 ,audit_count_records (self->audit_fd, &records));@@ -330,8 +441,8 @@ TEST_F(scoped_audit, connect_to_child) dgram_server =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , dgram_server); -ASSERT_EQ (0 ,bind (dgram_server, &self->dgram_address.unix_addr, - self->dgram_address.unix_addr_len)); +ASSERT_EQ (0 ,bind (dgram_server, &dgram_address->unix_addr, + dgram_address->unix_addr_len));/* Signals to the parent that child is listening. */ ASSERT_EQ (1 ,write (pipe_child[1 ],"." ,1 ));@@ -345,7 +456,9 @@ TEST_F(scoped_audit, connect_to_child)EXPECT_EQ (0 ,close (pipe_child[1 ]));EXPECT_EQ (0 ,close (pipe_parent[0 ])); -create_scoped_domain (_metadata,LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, +LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET );/* Signals that the parent is in a domain, if any. */ ASSERT_EQ (1 ,write (pipe_parent[1 ],"." ,1 ));@@ -355,19 +468,62 @@ TEST_F(scoped_audit, connect_to_child)/* Waits for the child to listen */ ASSERT_EQ (1 ,read (pipe_child[0 ], &buf,1 )); - err_dgram =connect (dgram_client, &self->dgram_address.unix_addr, - self->dgram_address.unix_addr_len); + err_dgram =connect (dgram_client, &dgram_address->unix_addr, + dgram_address->unix_addr_len);EXPECT_EQ (-1 , err_dgram);EXPECT_EQ (EPERM , errno); -EXPECT_EQ ( -0 , -audit_match_record ( - self->audit_fd,AUDIT_LANDLOCK_ACCESS , +if (variant->abstract_socket) { + log_match_cursor = stpncpy( + log_match, REGEX_LANDLOCK_PREFIX " blockers=scope\\.abstract_unix_socket path=" ABSTRACT_SOCKET_PATH_PREFIX "[0-9A-F]\\+$", -NULL ));
tools/testing/selftests/landlock/scoped_unix_test.c TEST_F(scoped_audit, connect_to_child) · line 491
Quoted 10 lines
tools/testing/selftests/landlock/scoped_unix_test.c renamed from tools/testing/selftests/landlock/scoped_abstract_unix_test.c +10
-0+ log_match_remaining); + log_match_remaining = +sizeof (log_match) - (log_match_cursor - log_match); +ASSERT_NE (0 , log_match_remaining); + }else { +/* +* It is assumed that absolute_path does not contain control +* characters nor spaces, see audit_string_contains_control(). +*/ + char *absolute_path =
const char *absolute_path
Quoted 42 lines
tools/testing/selftests/landlock/scoped_unix_test.c renamed from tools/testing/selftests/landlock/scoped_abstract_unix_test.c +35
-0+realpath (dgram_address->unix_addr.sun_path,NULL ); + + EXPECT_NE(NULL, absolute_path) + { +TH_LOG ("realpath() failed: %s" ,strerror (errno)); +return ; + } + + log_match_cursor = +stpncpy (log_match, +REGEX_LANDLOCK_PREFIX +" blockers=scope\\.pathname_unix_socket path=\"" , + log_match_remaining); + log_match_remaining = +sizeof (log_match) - (log_match_cursor - log_match); +ASSERT_NE (0 , log_match_remaining); + log_match_cursor =regex_escape (absolute_path, log_match_cursor, + log_match_remaining); +free (absolute_path); +if (log_match_cursor <0 ) { +TH_LOG ("regex_escape() failed (buffer too small)" ); +return ; + } + log_match_remaining = +sizeof (log_match) - (log_match_cursor - log_match); +ASSERT_NE (0 , log_match_remaining); + log_match_cursor = +stpncpy (log_match_cursor,"\"$" , log_match_remaining); + log_match_remaining = +sizeof (log_match) - (log_match_cursor - log_match); +ASSERT_NE (0 , log_match_remaining); + } + +EXPECT_EQ (0 ,audit_match_record (self->audit_fd,AUDIT_LANDLOCK_ACCESS , + log_match,NULL ));ASSERT_EQ (1 ,write (pipe_parent[1 ],"." ,1 ));EXPECT_EQ (0 ,close (dgram_client));Signature · 1 line
2.52.0
Re: [PATCH v2 5/6] selftests/landlock: Repurpose scoped_abstract_unix_test.c for pathname sockets too.
Quoted 14 lines · Mickaël Salaün
[...]
tools/testing/selftests/landlock/scoped_unix_test.c TEST_F(scoped_audit, connect_to_child) · line 419
Quoted 9 lines · Tingmao Wang
[...]
tools/testing/selftests/landlock/scoped_unix_test.c renamed from tools/testing/selftests/landlock/scoped_abstract_unix_test.c +4
-0@@ -308,6 +413,12 @@ TEST_F(scoped_audit, connect_to_child)char buf;int dgram_client;struct audit_records records; +struct service_fixture *const dgram_address = + variant->abstract_socket ? &self->dgram_address_abstract : + &self->dgram_address_pathname; +size_t log_match_remaining =500 ;const
Quoted 2 lines
Why this number? Could you please follow the same logic as in
matches_log_fs_extra()?
It's not a const since we decrement it below as we fill log_match with stpncpy().
matches_log_fs_extra() uses 2*PATH_MAX + length of various fixed strings. I guess since the path is a resolved absolute path which can vary based on where this test is ran, it is safest to use a value based on PATH_MAX. I will update.
Quoted 54 lines
Quoted 54 lines
tools/testing/selftests/landlock/scoped_unix_test.c renamed from tools/testing/selftests/landlock/scoped_abstract_unix_test.c +16
-10+char log_match[log_match_remaining]; +char *log_match_cursor = log_match;/* Makes sure there is no superfluous logged records. */ EXPECT_EQ (0 ,audit_count_records (self->audit_fd, &records));@@ -330,8 +441,8 @@ TEST_F(scoped_audit, connect_to_child) dgram_server =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , dgram_server); -ASSERT_EQ (0 ,bind (dgram_server, &self->dgram_address.unix_addr, - self->dgram_address.unix_addr_len)); +ASSERT_EQ (0 ,bind (dgram_server, &dgram_address->unix_addr, + dgram_address->unix_addr_len));/* Signals to the parent that child is listening. */ ASSERT_EQ (1 ,write (pipe_child[1 ],"." ,1 ));@@ -345,7 +456,9 @@ TEST_F(scoped_audit, connect_to_child)EXPECT_EQ (0 ,close (pipe_child[1 ]));EXPECT_EQ (0 ,close (pipe_parent[0 ])); -create_scoped_domain (_metadata,LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, +LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET );/* Signals that the parent is in a domain, if any. */ ASSERT_EQ (1 ,write (pipe_parent[1 ],"." ,1 ));@@ -355,19 +468,62 @@ TEST_F(scoped_audit, connect_to_child)/* Waits for the child to listen */ ASSERT_EQ (1 ,read (pipe_child[0 ], &buf,1 )); - err_dgram =connect (dgram_client, &self->dgram_address.unix_addr, - self->dgram_address.unix_addr_len); + err_dgram =connect (dgram_client, &dgram_address->unix_addr, + dgram_address->unix_addr_len);EXPECT_EQ (-1 , err_dgram);EXPECT_EQ (EPERM , errno); -EXPECT_EQ ( -0 , -audit_match_record ( - self->audit_fd,AUDIT_LANDLOCK_ACCESS , +if (variant->abstract_socket) { + log_match_cursor =stpncpy ( + log_match,REGEX_LANDLOCK_PREFIX " blockers=scope\\.abstract_unix_socket path=" ABSTRACT_SOCKET_PATH_PREFIX "[0-9A-F]\\+$" , -NULL )); + log_match_remaining); + log_match_remaining = +sizeof (log_match) - (log_match_cursor - log_match); +ASSERT_NE (0 , log_match_remaining);
Quoted 8 lines
tools/testing/selftests/landlock/scoped_unix_test.c TEST_F(scoped_audit, connect_to_child) · line 491
Quoted 6 lines
tools/testing/selftests/landlock/scoped_unix_test.c renamed from tools/testing/selftests/landlock/scoped_abstract_unix_test.c +6
-0+ }else { +/* +* It is assumed that absolute_path does not contain control +* characters nor spaces, see audit_string_contains_control(). +*/ + char *absolute_path =const char
*absolute_path
Can't use const char * here since we free() it later:
scoped_unix_test.c:513:22: warning: passing argument 1 of ‘free’ discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
513 | free(absolute_path);
| ^~~~~~~~~~~~~But I guess we can char *const. Will update to:
char *const absolute_path = realpath(dgram_address->unix_addr.sun_path, NULL);
Quoted 43 lines
Quoted 43 lines
tools/testing/selftests/landlock/scoped_unix_test.c renamed from tools/testing/selftests/landlock/scoped_abstract_unix_test.c +35
-0+realpath (dgram_address->unix_addr.sun_path,NULL ); + + EXPECT_NE(NULL, absolute_path) + { +TH_LOG ("realpath() failed: %s" ,strerror (errno)); +return ; + } + + log_match_cursor = +stpncpy (log_match, +REGEX_LANDLOCK_PREFIX +" blockers=scope\\.pathname_unix_socket path=\"" , + log_match_remaining); + log_match_remaining = +sizeof (log_match) - (log_match_cursor - log_match); +ASSERT_NE (0 , log_match_remaining); + log_match_cursor =regex_escape (absolute_path, log_match_cursor, + log_match_remaining); +free (absolute_path); +if (log_match_cursor <0 ) { +TH_LOG ("regex_escape() failed (buffer too small)" ); +return ; + } + log_match_remaining = +sizeof (log_match) - (log_match_cursor - log_match); +ASSERT_NE (0 , log_match_remaining); + log_match_cursor = +stpncpy (log_match_cursor,"\"$" , log_match_remaining); + log_match_remaining = +sizeof (log_match) - (log_match_cursor - log_match); +ASSERT_NE (0 , log_match_remaining); + } + +EXPECT_EQ (0 ,audit_match_record (self->audit_fd,AUDIT_LANDLOCK_ACCESS , + log_match,NULL ));ASSERT_EQ (1 ,write (pipe_parent[1 ],"." ,1 ));EXPECT_EQ (0 ,close (dgram_client));Signature · 1 line
2.52.0
PATCH v2 6/6 selftests/landlock: Add pathname socket variants for more tests
In reply to Tingmao WangWhile this produces a lot of change, it does allow us to "simultaneously" test both abstract and pathname UNIX sockets with reletively little code duplication, since they are really similar.
Tests touched: scoped_vs_unscoped, outside_socket, various_address_sockets, datagram_sockets, self_connect.
--- .../selftests/landlock/scoped_unix_test.c | 599 ++++++++++++------ 1 file changed, 395 insertions(+), 204 deletions(-)
tools/testing/selftests/landlock/scoped_unix_test.c +395 -204
@@ -536,8 +536,12 @@ TEST_F(scoped_audit, connect_to_child)FIXTURE (scoped_vs_unscoped) { -struct service_fixture parent_stream_address, parent_dgram_address, - child_stream_address, child_dgram_address; +struct service_fixture parent_stream_address_abstract, + parent_dgram_address_abstract, child_stream_address_abstract, + child_dgram_address_abstract; +struct service_fixture parent_stream_address_pathname, + parent_dgram_address_pathname, child_stream_address_pathname, + child_dgram_address_pathname; };#include "scoped_multiple_domain_variants.h"
@@ -546,35 +550,75 @@ FIXTURE_SETUP(scoped_vs_unscoped) {drop_caps (_metadata); -memset (&self->parent_stream_address,0 , -sizeof (self->parent_stream_address)); -set_unix_address (&self->parent_stream_address,0 , true); -memset (&self->parent_dgram_address,0 , -sizeof (self->parent_dgram_address)); -set_unix_address (&self->parent_dgram_address,1 , true); -memset (&self->child_stream_address,0 , -sizeof (self->child_stream_address)); -set_unix_address (&self->child_stream_address,2 , true); -memset (&self->child_dgram_address,0 , -sizeof (self->child_dgram_address)); -set_unix_address (&self->child_dgram_address,3 , true); +ASSERT_EQ (0 ,mkdir (PATHNAME_UNIX_SOCK_DIR ,0700 )); + +/* Abstract addresses. */ +memset (&self->parent_stream_address_abstract,0 , +sizeof (self->parent_stream_address_abstract)); +set_unix_address (&self->parent_stream_address_abstract,0 , true); +memset (&self->parent_dgram_address_abstract,0 , +sizeof (self->parent_dgram_address_abstract)); +set_unix_address (&self->parent_dgram_address_abstract,1 , true); +memset (&self->child_stream_address_abstract,0 , +sizeof (self->child_stream_address_abstract)); +set_unix_address (&self->child_stream_address_abstract,2 , true); +memset (&self->child_dgram_address_abstract,0 , +sizeof (self->child_dgram_address_abstract)); +set_unix_address (&self->child_dgram_address_abstract,3 , true); + +/* Pathname addresses. */ +memset (&self->parent_stream_address_pathname,0 , +sizeof (self->parent_stream_address_pathname)); +set_unix_address (&self->parent_stream_address_pathname,4 , false); +memset (&self->parent_dgram_address_pathname,0 , +sizeof (self->parent_dgram_address_pathname)); +set_unix_address (&self->parent_dgram_address_pathname,5 , false); +memset (&self->child_stream_address_pathname,0 , +sizeof (self->child_stream_address_pathname)); +set_unix_address (&self->child_stream_address_pathname,6 , false); +memset (&self->child_dgram_address_pathname,0 , +sizeof (self->child_dgram_address_pathname)); +set_unix_address (&self->child_dgram_address_pathname,7 , false); }FIXTURE_TEARDOWN (scoped_vs_unscoped) { +EXPECT_EQ (0 ,remove_path (self->parent_stream_address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,remove_path (self->parent_dgram_address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,remove_path (self->child_stream_address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,remove_path (self->child_dgram_address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,rmdir (PATHNAME_UNIX_SOCK_DIR )); }/* * Test unix_stream_connect and unix_may_send for parent, child and * grand child processes when they can have scoped or non-scoped domains. */ -TEST_F(scoped_vs_unscoped, unix_scoping) +static void test_scoped_vs_unscoped ( +struct __test_metadata *const _metadata, +FIXTURE_DATA (scoped_vs_unscoped) * self, +const FIXTURE_VARIANT (scoped_vs_unscoped) * variant, +const bool abstract) {pid_t child;int status;bool can_connect_to_parent, can_connect_to_child;int pipe_parent[2 ];int stream_server_parent, dgram_server_parent; +const __u16 scope = abstract ?LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET : +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ; +const struct service_fixture *parent_stream_address = + abstract ? &self->parent_stream_address_abstract : + &self->parent_stream_address_pathname; +const struct service_fixture *parent_dgram_address = + abstract ? &self->parent_dgram_address_abstract : + &self->parent_dgram_address_pathname; +const struct service_fixture *child_stream_address = + abstract ? &self->child_stream_address_abstract : + &self->child_stream_address_pathname; +const struct service_fixture *child_dgram_address = + abstract ? &self->child_dgram_address_abstract : + &self->child_dgram_address_pathname; can_connect_to_child = (variant->domain_grand_child !=SCOPE_SANDBOX ); can_connect_to_parent = (can_connect_to_child &&
@@ -585,8 +629,7 @@ TEST_F(scoped_vs_unscoped, unix_scoping)if (variant->domain_all ==OTHER_SANDBOX )create_fs_domain (_metadata);else if (variant->domain_all ==SCOPE_SANDBOX ) -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope); child =fork ();ASSERT_LE (0 , child);
@@ -600,8 +643,7 @@ TEST_F(scoped_vs_unscoped, unix_scoping)if (variant->domain_children ==OTHER_SANDBOX )create_fs_domain (_metadata);else if (variant->domain_children ==SCOPE_SANDBOX ) -create_scoped_domain ( - _metadata,LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope); grand_child =fork ();ASSERT_LE (0 , grand_child);
@@ -616,9 +658,7 @@ TEST_F(scoped_vs_unscoped, unix_scoping)if (variant->domain_grand_child ==OTHER_SANDBOX )create_fs_domain (_metadata);else if (variant->domain_grand_child ==SCOPE_SANDBOX ) -create_scoped_domain ( - _metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope); stream_client =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_client);
@@ -626,15 +666,13 @@ TEST_F(scoped_vs_unscoped, unix_scoping)ASSERT_LE (0 , dgram_client);ASSERT_EQ (1 ,read (pipe_child[0 ], &buf,1 )); - stream_err =connect ( - stream_client, - &self->child_stream_address.unix_addr, - self->child_stream_address.unix_addr_len); + stream_err =connect (stream_client, + &child_stream_address->unix_addr, + child_stream_address->unix_addr_len); stream_errno = errno; - dgram_err =connect ( - dgram_client, - &self->child_dgram_address.unix_addr, - self->child_dgram_address.unix_addr_len); + dgram_err =connect (dgram_client, + &child_dgram_address->unix_addr, + child_dgram_address->unix_addr_len); dgram_errno = errno;if (can_connect_to_child) {EXPECT_EQ (0 , stream_err);
@@ -653,14 +691,12 @@ TEST_F(scoped_vs_unscoped, unix_scoping)ASSERT_EQ (1 ,read (pipe_parent[0 ], &buf,1 )); stream_err =connect ( - stream_client, - &self->parent_stream_address.unix_addr, - self->parent_stream_address.unix_addr_len); + stream_client, &parent_stream_address->unix_addr, + parent_stream_address->unix_addr_len); stream_errno = errno; dgram_err =connect ( - dgram_client, - &self->parent_dgram_address.unix_addr, - self->parent_dgram_address.unix_addr_len); + dgram_client, &parent_dgram_address->unix_addr, + parent_dgram_address->unix_addr_len); dgram_errno = errno;if (can_connect_to_parent) {EXPECT_EQ (0 , stream_err);
@@ -681,8 +717,7 @@ TEST_F(scoped_vs_unscoped, unix_scoping)if (variant->domain_child ==OTHER_SANDBOX )create_fs_domain (_metadata);else if (variant->domain_child ==SCOPE_SANDBOX ) -create_scoped_domain ( - _metadata,LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope); stream_server_child =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_server_child);
@@ -690,11 +725,11 @@ TEST_F(scoped_vs_unscoped, unix_scoping)ASSERT_LE (0 , dgram_server_child);ASSERT_EQ (0 ,bind (stream_server_child, - &self->child_stream_address.unix_addr, - self->child_stream_address.unix_addr_len)); -ASSERT_EQ (0 ,bind (dgram_server_child, - &self->child_dgram_address.unix_addr, - self->child_dgram_address.unix_addr_len)); + &child_stream_address->unix_addr, + child_stream_address->unix_addr_len)); +ASSERT_EQ (0 , +bind (dgram_server_child, &child_dgram_address->unix_addr, + child_dgram_address->unix_addr_len));ASSERT_EQ (0 ,listen (stream_server_child, backlog));ASSERT_EQ (1 ,write (pipe_child[1 ],"." ,1 ));
@@ -708,19 +743,16 @@ TEST_F(scoped_vs_unscoped, unix_scoping)if (variant->domain_parent ==OTHER_SANDBOX )create_fs_domain (_metadata);else if (variant->domain_parent ==SCOPE_SANDBOX ) -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope); stream_server_parent =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_server_parent); dgram_server_parent =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , dgram_server_parent); -ASSERT_EQ (0 ,bind (stream_server_parent, - &self->parent_stream_address.unix_addr, - self->parent_stream_address.unix_addr_len)); -ASSERT_EQ (0 ,bind (dgram_server_parent, - &self->parent_dgram_address.unix_addr, - self->parent_dgram_address.unix_addr_len)); +ASSERT_EQ (0 ,bind (stream_server_parent, &parent_stream_address->unix_addr, + parent_stream_address->unix_addr_len)); +ASSERT_EQ (0 ,bind (dgram_server_parent, &parent_dgram_address->unix_addr, + parent_dgram_address->unix_addr_len));ASSERT_EQ (0 ,listen (stream_server_parent, backlog));
@@ -734,57 +766,119 @@ TEST_F(scoped_vs_unscoped, unix_scoping) _metadata->exit_code =KSFT_FAIL ; } +TEST_F(scoped_vs_unscoped, unix_scoping_abstract) +{ +test_scoped_vs_unscoped (_metadata, self, variant, true); +} + +TEST_F(scoped_vs_unscoped, unix_scoping_pathname) +{ +test_scoped_vs_unscoped (_metadata, self, variant, false); +} +FIXTURE (outside_socket) { -struct service_fixture address, transit_address; +struct service_fixture address_abstract, transit_address_abstract; +struct service_fixture address_pathname, transit_address_pathname; };FIXTURE_VARIANT (outside_socket) {const bool child_socket;const int type; +const bool abstract; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(outside_socket, abstract_allow_dgram_child) { +/* clang-format on */ + .child_socket = true, + .type = SOCK_DGRAM, + .abstract = true, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(outside_socket, abstract_deny_dgram_server) { +/* clang-format on */ + .child_socket = false, + .type = SOCK_DGRAM, + .abstract = true, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(outside_socket, abstract_allow_stream_child) { +/* clang-format on */ + .child_socket = true, + .type = SOCK_STREAM, + .abstract = true, };/* clang-format off */ -FIXTURE_VARIANT_ADD(outside_socket, allow_dgram_child) { +FIXTURE_VARIANT_ADD(outside_socket, abstract_deny_stream_server) { +/* clang-format on */ + .child_socket = false, + .type = SOCK_STREAM, + .abstract = true, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(outside_socket, pathname_allow_dgram_child) {/* clang-format on */ .child_socket = true, .type = SOCK_DGRAM, + .abstract = false, };/* clang-format off */ -FIXTURE_VARIANT_ADD(outside_socket, deny_dgram_server) { +FIXTURE_VARIANT_ADD(outside_socket, pathname_deny_dgram_server) {/* clang-format on */ .child_socket = false, .type = SOCK_DGRAM, + .abstract = false, };/* clang-format off */ -FIXTURE_VARIANT_ADD(outside_socket, allow_stream_child) { +FIXTURE_VARIANT_ADD(outside_socket, pathname_allow_stream_child) {/* clang-format on */ .child_socket = true, .type = SOCK_STREAM, + .abstract = false, };/* clang-format off */ -FIXTURE_VARIANT_ADD(outside_socket, deny_stream_server) { +FIXTURE_VARIANT_ADD(outside_socket, pathname_deny_stream_server) {/* clang-format on */ .child_socket = false, .type = SOCK_STREAM, + .abstract = false, };FIXTURE_SETUP (outside_socket) {drop_caps (_metadata); -memset (&self->transit_address,0 ,sizeof (self->transit_address)); -set_unix_address (&self->transit_address,0 , true); -memset (&self->address,0 ,sizeof (self->address)); -set_unix_address (&self->address,1 , true); +ASSERT_EQ (0 ,mkdir (PATHNAME_UNIX_SOCK_DIR ,0700 )); + +/* Abstract addresses. */ +memset (&self->transit_address_abstract,0 , +sizeof (self->transit_address_abstract)); +set_unix_address (&self->transit_address_abstract,0 , true); +memset (&self->address_abstract,0 ,sizeof (self->address_abstract)); +set_unix_address (&self->address_abstract,1 , true); + +/* Pathname addresses. */ +memset (&self->transit_address_pathname,0 , +sizeof (self->transit_address_pathname)); +set_unix_address (&self->transit_address_pathname,2 , false); +memset (&self->address_pathname,0 ,sizeof (self->address_pathname)); +set_unix_address (&self->address_pathname,3 , false); }FIXTURE_TEARDOWN (outside_socket) { +EXPECT_EQ (0 ,remove_path (self->transit_address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,remove_path (self->address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,rmdir (PATHNAME_UNIX_SOCK_DIR )); }/*
@@ -798,6 +892,15 @@ TEST_F(outside_socket, socket_with_different_domain)int pipe_child[2 ], pipe_parent[2 ];char buf_parent;int server_socket; +const __u16 scope = variant->abstract ? +LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET : +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ; +const struct service_fixture *transit_address = + variant->abstract ? &self->transit_address_abstract : + &self->transit_address_pathname; +const struct service_fixture *address = + variant->abstract ? &self->address_abstract : + &self->address_pathname;ASSERT_EQ (0 ,pipe2 (pipe_child,O_CLOEXEC ));ASSERT_EQ (0 ,pipe2 (pipe_parent,O_CLOEXEC ));
@@ -812,8 +915,7 @@ TEST_F(outside_socket, socket_with_different_domain)EXPECT_EQ (0 ,close (pipe_child[0 ]));/* Client always has a domain. */ -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope);if (variant->child_socket) {int data_socket, passed_socket, stream_server;
@@ -823,8 +925,8 @@ TEST_F(outside_socket, socket_with_different_domain) stream_server =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_server);ASSERT_EQ (0 ,bind (stream_server, - &self->transit_address.unix_addr, - self->transit_address.unix_addr_len)); + &transit_address->unix_addr, + transit_address->unix_addr_len));ASSERT_EQ (0 ,listen (stream_server, backlog));ASSERT_EQ (1 ,write (pipe_child[1 ],"." ,1 )); data_socket =accept (stream_server,NULL ,NULL );
@@ -839,8 +941,8 @@ TEST_F(outside_socket, socket_with_different_domain)/* Waits for parent signal for connection. */ ASSERT_EQ (1 ,read (pipe_parent[0 ], &buf_child,1 )); - err =connect (client_socket, &self->address.unix_addr, - self->address.unix_addr_len); + err =connect (client_socket, &address->unix_addr, + address->unix_addr_len);if (variant->child_socket) {EXPECT_EQ (0 , err); }else {
@@ -859,9 +961,8 @@ TEST_F(outside_socket, socket_with_different_domain)ASSERT_LE (0 , client_child);ASSERT_EQ (1 ,read (pipe_child[0 ], &buf_parent,1 )); -ASSERT_EQ (0 ,connect (client_child, - &self->transit_address.unix_addr, - self->transit_address.unix_addr_len)); +ASSERT_EQ (0 ,connect (client_child, &transit_address->unix_addr, + transit_address->unix_addr_len)); server_socket =recv_fd (client_child);EXPECT_EQ (0 ,close (client_child)); }else {
@@ -870,10 +971,10 @@ TEST_F(outside_socket, socket_with_different_domain)ASSERT_LE (0 , server_socket);/* Server always has a domain. */ -create_scoped_domain (_metadata,LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope); -ASSERT_EQ (0 ,bind (server_socket, &self->address.unix_addr, - self->address.unix_addr_len)); +ASSERT_EQ (0 , +bind (server_socket, &address->unix_addr, address->unix_addr_len));if (variant->type ==SOCK_STREAM )ASSERT_EQ (0 ,listen (server_socket, backlog));
@@ -888,52 +989,85 @@ TEST_F(outside_socket, socket_with_different_domain) _metadata->exit_code =KSFT_FAIL ; } -static const char stream_path[] =TMP_DIR "/stream.sock" ; -static const char dgram_path[] =TMP_DIR "/dgram.sock" ; -/* clang-format off */ -FIXTURE (various_address_sockets) {}; +FIXTURE (various_address_sockets) { +struct service_fixture stream_pathname_addr, dgram_pathname_addr; +struct service_fixture stream_abstract_addr, dgram_abstract_addr; +};/* clang-format on */ -FIXTURE_VARIANT (various_address_sockets) -{ -const int domain; +/* +* Test all 4 combinations of abstract and pathname socket scope bits, +* plus a case with no Landlock domain at all. +*/ +/* clang-format off */ +FIXTURE_VARIANT (various_address_sockets) { +/* clang-format on */ +const __u16 scope_bits; +const bool no_sandbox; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(various_address_sockets, scope_abstract) { +/* clang-format on */ + .scope_bits =LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET , +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(various_address_sockets, scope_pathname) { +/* clang-format on */ + .scope_bits =LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET , };/* clang-format off */ -FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_scoped_domain) { +FIXTURE_VARIANT_ADD(various_address_sockets, scope_both) {/* clang-format on */ - .domain =SCOPE_SANDBOX , + .scope_bits =LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET , };/* clang-format off */ -FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_other_domain) { +FIXTURE_VARIANT_ADD(various_address_sockets, scope_none) {/* clang-format on */ - .domain =OTHER_SANDBOX , + .scope_bits = 0, };/* clang-format off */ -FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_no_domain) { +FIXTURE_VARIANT_ADD(various_address_sockets, no_domain) {/* clang-format on */ - .domain = NO_SANDBOX, + .no_sandbox = true, };FIXTURE_SETUP (various_address_sockets) {drop_caps (_metadata); -umask (0077 ); -ASSERT_EQ (0 ,mkdir (TMP_DIR ,0700 )); +ASSERT_EQ (0 ,mkdir (PATHNAME_UNIX_SOCK_DIR ,0700 )); + +memset (&self->stream_pathname_addr,0 ,sizeof (self->stream_pathname_addr)); +set_unix_address (&self->stream_pathname_addr,0 , false); +memset (&self->dgram_pathname_addr,0 ,sizeof (self->dgram_pathname_addr)); +set_unix_address (&self->dgram_pathname_addr,1 , false); + +memset (&self->stream_abstract_addr,0 ,sizeof (self->stream_abstract_addr)); +set_unix_address (&self->stream_abstract_addr,2 , true); +memset (&self->dgram_abstract_addr,0 ,sizeof (self->dgram_abstract_addr)); +set_unix_address (&self->dgram_abstract_addr,3 , true); }FIXTURE_TEARDOWN (various_address_sockets) { -EXPECT_EQ (0 ,unlink (stream_path)); -EXPECT_EQ (0 ,unlink (dgram_path)); -EXPECT_EQ (0 ,rmdir (TMP_DIR )); +EXPECT_EQ (0 ,remove_path (self->stream_pathname_addr.unix_addr.sun_path)); +EXPECT_EQ (0 ,remove_path (self->dgram_pathname_addr.unix_addr.sun_path)); +EXPECT_EQ (0 ,rmdir (PATHNAME_UNIX_SOCK_DIR )); } -TEST_F(various_address_sockets, scoped_pathname_sockets) +/* +* Test interaction of various scope flags (controlled by variant->domain) +* with pathname and abstract sockets when connecting from a sandboxed +* child. +*/ +TEST_F(various_address_sockets, scoped_sockets) {pid_t child;int status;
@@ -942,25 +1076,10 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)int unnamed_sockets[2 ];int stream_pathname_socket, dgram_pathname_socket, stream_abstract_socket, dgram_abstract_socket, data_socket; -struct service_fixture stream_abstract_addr, dgram_abstract_addr; -struct sockaddr_un stream_pathname_addr = { - .sun_family =AF_UNIX , - }; -struct sockaddr_un dgram_pathname_addr = { - .sun_family =AF_UNIX , - }; - -/* Pathname address. */ -snprintf (stream_pathname_addr.sun_path, -sizeof (stream_pathname_addr.sun_path),"%s" , stream_path); -snprintf (dgram_pathname_addr.sun_path, -sizeof (dgram_pathname_addr.sun_path),"%s" , dgram_path); - -/* Abstract address. */ -memset (&stream_abstract_addr,0 ,sizeof (stream_abstract_addr)); -set_unix_address (&stream_abstract_addr,0 , true); -memset (&dgram_abstract_addr,0 ,sizeof (dgram_abstract_addr)); -set_unix_address (&dgram_abstract_addr,1 , true); +bool pathname_restricted = + (variant->scope_bits &LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ); +bool abstract_restricted = + (variant->scope_bits &LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET );/* Unnamed address for datagram socket. */ ASSERT_EQ (0 ,socketpair (AF_UNIX ,SOCK_DGRAM ,0 , unnamed_sockets));
@@ -975,82 +1094,103 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)EXPECT_EQ (0 ,close (pipe_parent[1 ]));EXPECT_EQ (0 ,close (unnamed_sockets[1 ])); -if (variant->domain ==SCOPE_SANDBOX ) -create_scoped_domain ( - _metadata,LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); -else if (variant->domain ==OTHER_SANDBOX ) +/* Create domain based on variant. */ +if (variant->scope_bits) +create_scoped_domain (_metadata, variant->scope_bits); +else if (!variant->no_sandbox)create_fs_domain (_metadata);/* Waits for parent to listen. */ ASSERT_EQ (1 ,read (pipe_parent[0 ], &buf_child,1 ));EXPECT_EQ (0 ,close (pipe_parent[0 ])); -/* Checks that we can send data through a datagram socket. */ +/* Checks that we can send data through a unnamed socket. */ ASSERT_EQ (1 ,write (unnamed_sockets[0 ],"a" ,1 ));EXPECT_EQ (0 ,close (unnamed_sockets[0 ]));/* Connects with pathname sockets. */ stream_pathname_socket =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_pathname_socket); -ASSERT_EQ (0 , -connect (stream_pathname_socket, &stream_pathname_addr, -sizeof (stream_pathname_addr))); -ASSERT_EQ (1 ,write (stream_pathname_socket,"b" ,1 )); + err =connect (stream_pathname_socket, + &self->stream_pathname_addr.unix_addr, + self->stream_pathname_addr.unix_addr_len); +if (pathname_restricted) { +EXPECT_EQ (-1 , err); +EXPECT_EQ (EPERM , errno); + }else { +EXPECT_EQ (0 , err); +ASSERT_EQ (1 ,write (stream_pathname_socket,"b" ,1 )); + }EXPECT_EQ (0 ,close (stream_pathname_socket)); -/* Sends without connection. */ +/* Sends without connection (pathname). */ dgram_pathname_socket =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , dgram_pathname_socket); err =sendto (dgram_pathname_socket,"c" ,1 ,0 , - &dgram_pathname_addr,sizeof (dgram_pathname_addr)); -EXPECT_EQ (1 , err); + &self->dgram_pathname_addr.unix_addr, + self->dgram_pathname_addr.unix_addr_len); +if (pathname_restricted) { +EXPECT_EQ (-1 , err); +EXPECT_EQ (EPERM , errno); + }else { +EXPECT_EQ (1 , err); + } + +/* Sends with connection (pathname). */ + err =connect (dgram_pathname_socket, + &self->dgram_pathname_addr.unix_addr, + self->dgram_pathname_addr.unix_addr_len); +if (pathname_restricted) { +EXPECT_EQ (-1 , err); +EXPECT_EQ (EPERM , errno); + }else { +EXPECT_EQ (0 , err); +ASSERT_EQ (1 ,write (dgram_pathname_socket,"d" ,1 )); + } -/* Sends with connection. */ -ASSERT_EQ (0 , -connect (dgram_pathname_socket, &dgram_pathname_addr, -sizeof (dgram_pathname_addr))); -ASSERT_EQ (1 ,write (dgram_pathname_socket,"d" ,1 ));EXPECT_EQ (0 ,close (dgram_pathname_socket));/* Connects with abstract sockets. */ stream_abstract_socket =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_abstract_socket); err =connect (stream_abstract_socket, - &stream_abstract_addr.unix_addr, - stream_abstract_addr.unix_addr_len); -if (variant->domain ==SCOPE_SANDBOX ) { + &self->stream_abstract_addr.unix_addr, + self->stream_abstract_addr.unix_addr_len); +if (abstract_restricted) {EXPECT_EQ (-1 , err);EXPECT_EQ (EPERM , errno); }else {EXPECT_EQ (0 , err);ASSERT_EQ (1 ,write (stream_abstract_socket,"e" ,1 )); } +EXPECT_EQ (0 ,close (stream_abstract_socket)); -/* Sends without connection. */ +/* Sends without connection (abstract). */ dgram_abstract_socket =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , dgram_abstract_socket); err =sendto (dgram_abstract_socket,"f" ,1 ,0 , - &dgram_abstract_addr.unix_addr, - dgram_abstract_addr.unix_addr_len); -if (variant->domain ==SCOPE_SANDBOX ) { + &self->dgram_abstract_addr.unix_addr, + self->dgram_abstract_addr.unix_addr_len); +if (abstract_restricted) {EXPECT_EQ (-1 , err);EXPECT_EQ (EPERM , errno); }else {EXPECT_EQ (1 , err); } -/* Sends with connection. */ +/* Sends with connection (abstract). */ err =connect (dgram_abstract_socket, - &dgram_abstract_addr.unix_addr, - dgram_abstract_addr.unix_addr_len); -if (variant->domain ==SCOPE_SANDBOX ) { + &self->dgram_abstract_addr.unix_addr, + self->dgram_abstract_addr.unix_addr_len); +if (abstract_restricted) {EXPECT_EQ (-1 , err);EXPECT_EQ (EPERM , errno); }else {EXPECT_EQ (0 , err);ASSERT_EQ (1 ,write (dgram_abstract_socket,"g" ,1 )); } +EXPECT_EQ (0 ,close (dgram_abstract_socket));_exit (_metadata->exit_code);
@@ -1062,27 +1202,30 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)/* Sets up pathname servers. */ stream_pathname_socket =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_pathname_socket); -ASSERT_EQ (0 ,bind (stream_pathname_socket, &stream_pathname_addr, -sizeof (stream_pathname_addr))); +ASSERT_EQ (0 ,bind (stream_pathname_socket, + &self->stream_pathname_addr.unix_addr, + self->stream_pathname_addr.unix_addr_len));ASSERT_EQ (0 ,listen (stream_pathname_socket, backlog)); dgram_pathname_socket =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , dgram_pathname_socket); -ASSERT_EQ (0 ,bind (dgram_pathname_socket, &dgram_pathname_addr, -sizeof (dgram_pathname_addr))); +ASSERT_EQ (0 ,bind (dgram_pathname_socket, + &self->dgram_pathname_addr.unix_addr, + self->dgram_pathname_addr.unix_addr_len));/* Sets up abstract servers. */ stream_abstract_socket =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_abstract_socket); -ASSERT_EQ (0 , -bind (stream_abstract_socket, &stream_abstract_addr.unix_addr, - stream_abstract_addr.unix_addr_len)); +ASSERT_EQ (0 ,bind (stream_abstract_socket, + &self->stream_abstract_addr.unix_addr, + self->stream_abstract_addr.unix_addr_len)); +ASSERT_EQ (0 ,listen (stream_abstract_socket, backlog)); dgram_abstract_socket =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , dgram_abstract_socket); -ASSERT_EQ (0 ,bind (dgram_abstract_socket, &dgram_abstract_addr.unix_addr, - dgram_abstract_addr.unix_addr_len)); -ASSERT_EQ (0 ,listen (stream_abstract_socket, backlog)); +ASSERT_EQ (0 ,bind (dgram_abstract_socket, + &self->dgram_abstract_addr.unix_addr, + self->dgram_abstract_addr.unix_addr_len));ASSERT_EQ (1 ,write (pipe_parent[1 ],"." ,1 ));EXPECT_EQ (0 ,close (pipe_parent[1 ]));
@@ -1092,24 +1235,31 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)ASSERT_EQ ('a' , buf_parent);EXPECT_LE (0 ,close (unnamed_sockets[1 ])); -/* Reads from pathname sockets. */ - data_socket =accept (stream_pathname_socket,NULL ,NULL ); -ASSERT_LE (0 , data_socket); -ASSERT_EQ (1 ,read (data_socket, &buf_parent,sizeof (buf_parent))); -ASSERT_EQ ('b' , buf_parent); -EXPECT_EQ (0 ,close (data_socket)); -EXPECT_EQ (0 ,close (stream_pathname_socket)); +if (!pathname_restricted) { +/* +* Reads from pathname sockets if we expect child to be able to +* send. +*/ + data_socket =accept (stream_pathname_socket,NULL ,NULL ); +ASSERT_LE (0 , data_socket); +ASSERT_EQ (1 , +read (data_socket, &buf_parent,sizeof (buf_parent))); +ASSERT_EQ ('b' , buf_parent); +EXPECT_EQ (0 ,close (data_socket)); -ASSERT_EQ (1 , -read (dgram_pathname_socket, &buf_parent,sizeof (buf_parent))); -ASSERT_EQ ('c' , buf_parent); -ASSERT_EQ (1 , -read (dgram_pathname_socket, &buf_parent,sizeof (buf_parent))); -ASSERT_EQ ('d' , buf_parent); -EXPECT_EQ (0 ,close (dgram_pathname_socket)); +ASSERT_EQ (1 ,read (dgram_pathname_socket, &buf_parent, +sizeof (buf_parent))); +ASSERT_EQ ('c' , buf_parent); +ASSERT_EQ (1 ,read (dgram_pathname_socket, &buf_parent, +sizeof (buf_parent))); +ASSERT_EQ ('d' , buf_parent); + } -if (variant->domain !=SCOPE_SANDBOX ) { -/* Reads from abstract sockets if allowed to send. */ +if (!abstract_restricted) { +/* +* Reads from abstract sockets if we expect child to be able to +* send. +*/ data_socket =accept (stream_abstract_socket,NULL ,NULL );ASSERT_LE (0 , data_socket); ASSERT_EQ(1,
@@ -1125,30 +1275,73 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)ASSERT_EQ ('g' , buf_parent); } -/* Waits for all abstract socket tests. */ +/* Waits for child to complete, and only close the socket afterwards. */ ASSERT_EQ (child,waitpid (child, &status,0 ));EXPECT_EQ (0 ,close (stream_abstract_socket));EXPECT_EQ (0 ,close (dgram_abstract_socket)); +EXPECT_EQ (0 ,close (stream_pathname_socket)); +EXPECT_EQ (0 ,close (dgram_pathname_socket));if (WIFSIGNALED (status) || !WIFEXITED (status) ||WEXITSTATUS (status) !=EXIT_SUCCESS ) _metadata->exit_code =KSFT_FAIL ; } -TEST (datagram_sockets) +/* Fixture for datagram_sockets and self_connect tests */ +FIXTURE (socket_type_test) {struct service_fixture connected_addr, non_connected_addr; +}; + +FIXTURE_VARIANT (socket_type_test) +{ +const bool abstract; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(socket_type_test, abstract) { +/* clang-format on */ + .abstract = true, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(socket_type_test, pathname) { +/* clang-format on */ + .abstract = false, +}; + +FIXTURE_SETUP (socket_type_test) +{ +drop_caps (_metadata); + +if (!variant->abstract) +ASSERT_EQ (0 ,mkdir (PATHNAME_UNIX_SOCK_DIR ,0700 )); + +memset (&self->connected_addr,0 ,sizeof (self->connected_addr)); +set_unix_address (&self->connected_addr,0 , variant->abstract); +memset (&self->non_connected_addr,0 ,sizeof (self->non_connected_addr)); +set_unix_address (&self->non_connected_addr,1 , variant->abstract); +} + +FIXTURE_TEARDOWN (socket_type_test) +{ +if (!variant->abstract) { +EXPECT_EQ (0 ,remove_path (self->connected_addr.unix_addr.sun_path)); +EXPECT_EQ (0 ,remove_path (self->non_connected_addr.unix_addr.sun_path)); +EXPECT_EQ (0 ,rmdir (PATHNAME_UNIX_SOCK_DIR )); + } +} + +TEST_F(socket_type_test, datagram_sockets) +{int server_conn_socket, server_unconn_socket;int pipe_parent[2 ], pipe_child[2 ];int status;char buf;pid_t child; - -drop_caps (_metadata); -memset (&connected_addr,0 ,sizeof (connected_addr)); -set_unix_address (&connected_addr,0 , true); -memset (&non_connected_addr,0 ,sizeof (non_connected_addr)); -set_unix_address (&non_connected_addr,1 , true); +const __u16 scope = variant->abstract ? +LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET : +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ;ASSERT_EQ (0 ,pipe2 (pipe_parent,O_CLOEXEC ));ASSERT_EQ (0 ,pipe2 (pipe_child,O_CLOEXEC ));
@@ -1169,8 +1362,9 @@ TEST(datagram_sockets)/* Waits for parent to listen. */ ASSERT_EQ (1 ,read (pipe_parent[0 ], &buf,1 ));ASSERT_EQ (0 , -connect (client_conn_socket, &connected_addr.unix_addr, - connected_addr.unix_addr_len)); +connect (client_conn_socket, + &self->connected_addr.unix_addr, + self->connected_addr.unix_addr_len));/* * Both connected and non-connected sockets can send data when
@@ -1178,13 +1372,12 @@ TEST(datagram_sockets)*/ ASSERT_EQ (1 ,send (client_conn_socket,"." ,1 ,0 ));ASSERT_EQ (1 ,sendto (client_unconn_socket,"." ,1 ,0 , - &non_connected_addr.unix_addr, - non_connected_addr.unix_addr_len)); + &self->non_connected_addr.unix_addr, + self->non_connected_addr.unix_addr_len));ASSERT_EQ (1 ,write (pipe_child[1 ],"." ,1 ));/* Scopes the domain. */ -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope);/* * Connected socket sends data to the receiver, but the
@@ -1192,8 +1385,8 @@ TEST(datagram_sockets)*/ ASSERT_EQ (1 ,send (client_conn_socket,"." ,1 ,0 ));ASSERT_EQ (-1 ,sendto (client_unconn_socket,"." ,1 ,0 , - &non_connected_addr.unix_addr, - non_connected_addr.unix_addr_len)); + &self->non_connected_addr.unix_addr, + self->non_connected_addr.unix_addr_len));ASSERT_EQ (EPERM , errno);ASSERT_EQ (1 ,write (pipe_child[1 ],"." ,1 ));
@@ -1210,10 +1403,11 @@ TEST(datagram_sockets)ASSERT_LE (0 , server_conn_socket);ASSERT_LE (0 , server_unconn_socket); -ASSERT_EQ (0 ,bind (server_conn_socket, &connected_addr.unix_addr, - connected_addr.unix_addr_len)); -ASSERT_EQ (0 ,bind (server_unconn_socket, &non_connected_addr.unix_addr, - non_connected_addr.unix_addr_len)); +ASSERT_EQ (0 ,bind (server_conn_socket, &self->connected_addr.unix_addr, + self->connected_addr.unix_addr_len)); +ASSERT_EQ (0 ,bind (server_unconn_socket, + &self->non_connected_addr.unix_addr, + self->non_connected_addr.unix_addr_len));ASSERT_EQ (1 ,write (pipe_parent[1 ],"." ,1 ));/* Waits for child to test. */
@@ -1238,52 +1432,49 @@ TEST(datagram_sockets) _metadata->exit_code =KSFT_FAIL ; } -TEST (self_connect) +TEST_F(socket_type_test, self_connect) { -struct service_fixture connected_addr, non_connected_addr;int connected_socket, non_connected_socket, status;pid_t child; - -drop_caps (_metadata); -memset (&connected_addr,0 ,sizeof (connected_addr)); -set_unix_address (&connected_addr,0 , true); -memset (&non_connected_addr,0 ,sizeof (non_connected_addr)); -set_unix_address (&non_connected_addr,1 , true); +const __u16 scope = variant->abstract ? +LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET : +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ; connected_socket =socket (AF_UNIX ,SOCK_DGRAM ,0 ); non_connected_socket =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , connected_socket);ASSERT_LE (0 , non_connected_socket); -ASSERT_EQ (0 ,bind (connected_socket, &connected_addr.unix_addr, - connected_addr.unix_addr_len)); -ASSERT_EQ (0 ,bind (non_connected_socket, &non_connected_addr.unix_addr, - non_connected_addr.unix_addr_len)); +ASSERT_EQ (0 ,bind (connected_socket, &self->connected_addr.unix_addr, + self->connected_addr.unix_addr_len)); +ASSERT_EQ (0 ,bind (non_connected_socket, + &self->non_connected_addr.unix_addr, + self->non_connected_addr.unix_addr_len)); child =fork ();ASSERT_LE (0 , child);if (child ==0 ) {/* Child's domain is scoped. */ -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope);/* * The child inherits the sockets, and cannot connect or * send data to them. */ ASSERT_EQ (-1 , -connect (connected_socket, &connected_addr.unix_addr, - connected_addr.unix_addr_len)); +connect (connected_socket, + &self->connected_addr.unix_addr, + self->connected_addr.unix_addr_len));ASSERT_EQ (EPERM , errno);ASSERT_EQ (-1 ,sendto (connected_socket,"." ,1 ,0 , - &connected_addr.unix_addr, - connected_addr.unix_addr_len)); + &self->connected_addr.unix_addr, + self->connected_addr.unix_addr_len));ASSERT_EQ (EPERM , errno);ASSERT_EQ (-1 ,sendto (non_connected_socket,"." ,1 ,0 , - &non_connected_addr.unix_addr, - non_connected_addr.unix_addr_len)); + &self->non_connected_addr.unix_addr, + self->non_connected_addr.unix_addr_len));ASSERT_EQ (EPERM , errno);EXPECT_EQ (0 ,close (connected_socket));
Signature · 1 line
2.52.0
Mickaël Salaün
maintainerRe: [PATCH v2 6/6] selftests/landlock: Add pathname socket variants for more tests
The whole series looks good, thanks!
Quoted 592 lines · Tingmao Wang
While this produces a lot of change, it does allow us to "simultaneously" test both abstract and pathname UNIX sockets with reletively little code duplication, since they are really similar.
Tests touched:
scoped_vs_unscoped,outside_socket,various_address_sockets,datagram_sockets,self_connect.Signed-off-by Tingmao Wang <[email protected]>--- .../selftests/landlock/scoped_unix_test.c | 599 ++++++++++++------ 1 file changed, 395 insertions(+), 204 deletions(-)tools/testing/selftests/landlock/scoped_unix_test.c +231
-114@@ -536,8 +536,12 @@ TEST_F(scoped_audit, connect_to_child)FIXTURE (scoped_vs_unscoped) { -struct service_fixture parent_stream_address, parent_dgram_address, - child_stream_address, child_dgram_address; +struct service_fixture parent_stream_address_abstract, + parent_dgram_address_abstract, child_stream_address_abstract, + child_dgram_address_abstract; +struct service_fixture parent_stream_address_pathname, + parent_dgram_address_pathname, child_stream_address_pathname, + child_dgram_address_pathname; };#include "scoped_multiple_domain_variants.h" @@ -546,35 +550,75 @@ FIXTURE_SETUP(scoped_vs_unscoped) {drop_caps (_metadata); -memset (&self->parent_stream_address,0 , -sizeof (self->parent_stream_address)); -set_unix_address (&self->parent_stream_address,0 , true); -memset (&self->parent_dgram_address,0 , -sizeof (self->parent_dgram_address)); -set_unix_address (&self->parent_dgram_address,1 , true); -memset (&self->child_stream_address,0 , -sizeof (self->child_stream_address)); -set_unix_address (&self->child_stream_address,2 , true); -memset (&self->child_dgram_address,0 , -sizeof (self->child_dgram_address)); -set_unix_address (&self->child_dgram_address,3 , true); +ASSERT_EQ (0 ,mkdir (PATHNAME_UNIX_SOCK_DIR ,0700 )); + +/* Abstract addresses. */ +memset (&self->parent_stream_address_abstract,0 , +sizeof (self->parent_stream_address_abstract)); +set_unix_address (&self->parent_stream_address_abstract,0 , true); +memset (&self->parent_dgram_address_abstract,0 , +sizeof (self->parent_dgram_address_abstract)); +set_unix_address (&self->parent_dgram_address_abstract,1 , true); +memset (&self->child_stream_address_abstract,0 , +sizeof (self->child_stream_address_abstract)); +set_unix_address (&self->child_stream_address_abstract,2 , true); +memset (&self->child_dgram_address_abstract,0 , +sizeof (self->child_dgram_address_abstract)); +set_unix_address (&self->child_dgram_address_abstract,3 , true); + +/* Pathname addresses. */ +memset (&self->parent_stream_address_pathname,0 , +sizeof (self->parent_stream_address_pathname)); +set_unix_address (&self->parent_stream_address_pathname,4 , false); +memset (&self->parent_dgram_address_pathname,0 , +sizeof (self->parent_dgram_address_pathname)); +set_unix_address (&self->parent_dgram_address_pathname,5 , false); +memset (&self->child_stream_address_pathname,0 , +sizeof (self->child_stream_address_pathname)); +set_unix_address (&self->child_stream_address_pathname,6 , false); +memset (&self->child_dgram_address_pathname,0 , +sizeof (self->child_dgram_address_pathname)); +set_unix_address (&self->child_dgram_address_pathname,7 , false); }FIXTURE_TEARDOWN (scoped_vs_unscoped) { +EXPECT_EQ (0 ,remove_path (self->parent_stream_address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,remove_path (self->parent_dgram_address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,remove_path (self->child_stream_address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,remove_path (self->child_dgram_address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,rmdir (PATHNAME_UNIX_SOCK_DIR )); }/* * Test unix_stream_connect and unix_may_send for parent, child and * grand child processes when they can have scoped or non-scoped domains. */ -TEST_F(scoped_vs_unscoped, unix_scoping) +static void test_scoped_vs_unscoped ( +struct __test_metadata *const _metadata, +FIXTURE_DATA (scoped_vs_unscoped) * self, +const FIXTURE_VARIANT (scoped_vs_unscoped) * variant, +const bool abstract) {pid_t child;int status;bool can_connect_to_parent, can_connect_to_child;int pipe_parent[2 ];int stream_server_parent, dgram_server_parent; +const __u16 scope = abstract ?LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET : +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ; +const struct service_fixture *parent_stream_address = + abstract ? &self->parent_stream_address_abstract : + &self->parent_stream_address_pathname; +const struct service_fixture *parent_dgram_address = + abstract ? &self->parent_dgram_address_abstract : + &self->parent_dgram_address_pathname; +const struct service_fixture *child_stream_address = + abstract ? &self->child_stream_address_abstract : + &self->child_stream_address_pathname; +const struct service_fixture *child_dgram_address = + abstract ? &self->child_dgram_address_abstract : + &self->child_dgram_address_pathname; can_connect_to_child = (variant->domain_grand_child !=SCOPE_SANDBOX ); can_connect_to_parent = (can_connect_to_child &&@@ -585,8 +629,7 @@ TEST_F(scoped_vs_unscoped, unix_scoping)if (variant->domain_all ==OTHER_SANDBOX )create_fs_domain (_metadata);else if (variant->domain_all ==SCOPE_SANDBOX ) -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope); child =fork ();ASSERT_LE (0 , child);@@ -600,8 +643,7 @@ TEST_F(scoped_vs_unscoped, unix_scoping)if (variant->domain_children ==OTHER_SANDBOX )create_fs_domain (_metadata);else if (variant->domain_children ==SCOPE_SANDBOX ) -create_scoped_domain ( - _metadata,LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope); grand_child =fork ();ASSERT_LE (0 , grand_child);@@ -616,9 +658,7 @@ TEST_F(scoped_vs_unscoped, unix_scoping)if (variant->domain_grand_child ==OTHER_SANDBOX )create_fs_domain (_metadata);else if (variant->domain_grand_child ==SCOPE_SANDBOX ) -create_scoped_domain ( - _metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope); stream_client =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_client);@@ -626,15 +666,13 @@ TEST_F(scoped_vs_unscoped, unix_scoping)ASSERT_LE (0 , dgram_client);ASSERT_EQ (1 ,read (pipe_child[0 ], &buf,1 )); - stream_err =connect ( - stream_client, - &self->child_stream_address.unix_addr, - self->child_stream_address.unix_addr_len); + stream_err =connect (stream_client, + &child_stream_address->unix_addr, + child_stream_address->unix_addr_len); stream_errno = errno; - dgram_err =connect ( - dgram_client, - &self->child_dgram_address.unix_addr, - self->child_dgram_address.unix_addr_len); + dgram_err =connect (dgram_client, + &child_dgram_address->unix_addr, + child_dgram_address->unix_addr_len); dgram_errno = errno;if (can_connect_to_child) {EXPECT_EQ (0 , stream_err);@@ -653,14 +691,12 @@ TEST_F(scoped_vs_unscoped, unix_scoping)ASSERT_EQ (1 ,read (pipe_parent[0 ], &buf,1 )); stream_err =connect ( - stream_client, - &self->parent_stream_address.unix_addr, - self->parent_stream_address.unix_addr_len); + stream_client, &parent_stream_address->unix_addr, + parent_stream_address->unix_addr_len); stream_errno = errno; dgram_err =connect ( - dgram_client, - &self->parent_dgram_address.unix_addr, - self->parent_dgram_address.unix_addr_len); + dgram_client, &parent_dgram_address->unix_addr, + parent_dgram_address->unix_addr_len); dgram_errno = errno;if (can_connect_to_parent) {EXPECT_EQ (0 , stream_err);@@ -681,8 +717,7 @@ TEST_F(scoped_vs_unscoped, unix_scoping)if (variant->domain_child ==OTHER_SANDBOX )create_fs_domain (_metadata);else if (variant->domain_child ==SCOPE_SANDBOX ) -create_scoped_domain ( - _metadata,LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope); stream_server_child =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_server_child);@@ -690,11 +725,11 @@ TEST_F(scoped_vs_unscoped, unix_scoping)ASSERT_LE (0 , dgram_server_child);ASSERT_EQ (0 ,bind (stream_server_child, - &self->child_stream_address.unix_addr, - self->child_stream_address.unix_addr_len)); -ASSERT_EQ (0 ,bind (dgram_server_child, - &self->child_dgram_address.unix_addr, - self->child_dgram_address.unix_addr_len)); + &child_stream_address->unix_addr, + child_stream_address->unix_addr_len)); +ASSERT_EQ (0 , +bind (dgram_server_child, &child_dgram_address->unix_addr, + child_dgram_address->unix_addr_len));ASSERT_EQ (0 ,listen (stream_server_child, backlog));ASSERT_EQ (1 ,write (pipe_child[1 ],"." ,1 ));@@ -708,19 +743,16 @@ TEST_F(scoped_vs_unscoped, unix_scoping)if (variant->domain_parent ==OTHER_SANDBOX )create_fs_domain (_metadata);else if (variant->domain_parent ==SCOPE_SANDBOX ) -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope); stream_server_parent =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_server_parent); dgram_server_parent =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , dgram_server_parent); -ASSERT_EQ (0 ,bind (stream_server_parent, - &self->parent_stream_address.unix_addr, - self->parent_stream_address.unix_addr_len)); -ASSERT_EQ (0 ,bind (dgram_server_parent, - &self->parent_dgram_address.unix_addr, - self->parent_dgram_address.unix_addr_len)); +ASSERT_EQ (0 ,bind (stream_server_parent, &parent_stream_address->unix_addr, + parent_stream_address->unix_addr_len)); +ASSERT_EQ (0 ,bind (dgram_server_parent, &parent_dgram_address->unix_addr, + parent_dgram_address->unix_addr_len));ASSERT_EQ (0 ,listen (stream_server_parent, backlog));@@ -734,57 +766,119 @@ TEST_F(scoped_vs_unscoped, unix_scoping) _metadata->exit_code =KSFT_FAIL ; } +TEST_F(scoped_vs_unscoped, unix_scoping_abstract) +{ +test_scoped_vs_unscoped (_metadata, self, variant, true); +} + +TEST_F(scoped_vs_unscoped, unix_scoping_pathname) +{ +test_scoped_vs_unscoped (_metadata, self, variant, false); +} +FIXTURE (outside_socket) { -struct service_fixture address, transit_address; +struct service_fixture address_abstract, transit_address_abstract; +struct service_fixture address_pathname, transit_address_pathname; };FIXTURE_VARIANT (outside_socket) {const bool child_socket;const int type; +const bool abstract; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(outside_socket, abstract_allow_dgram_child) { +/* clang-format on */ + .child_socket = true, + .type = SOCK_DGRAM, + .abstract = true, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(outside_socket, abstract_deny_dgram_server) { +/* clang-format on */ + .child_socket = false, + .type = SOCK_DGRAM, + .abstract = true, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(outside_socket, abstract_allow_stream_child) { +/* clang-format on */ + .child_socket = true, + .type = SOCK_STREAM, + .abstract = true, };/* clang-format off */ -FIXTURE_VARIANT_ADD(outside_socket, allow_dgram_child) { +FIXTURE_VARIANT_ADD(outside_socket, abstract_deny_stream_server) { +/* clang-format on */ + .child_socket = false, + .type = SOCK_STREAM, + .abstract = true, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(outside_socket, pathname_allow_dgram_child) {/* clang-format on */ .child_socket = true, .type = SOCK_DGRAM, + .abstract = false, };/* clang-format off */ -FIXTURE_VARIANT_ADD(outside_socket, deny_dgram_server) { +FIXTURE_VARIANT_ADD(outside_socket, pathname_deny_dgram_server) {/* clang-format on */ .child_socket = false, .type = SOCK_DGRAM, + .abstract = false, };/* clang-format off */ -FIXTURE_VARIANT_ADD(outside_socket, allow_stream_child) { +FIXTURE_VARIANT_ADD(outside_socket, pathname_allow_stream_child) {/* clang-format on */ .child_socket = true, .type = SOCK_STREAM, + .abstract = false, };/* clang-format off */ -FIXTURE_VARIANT_ADD(outside_socket, deny_stream_server) { +FIXTURE_VARIANT_ADD(outside_socket, pathname_deny_stream_server) {/* clang-format on */ .child_socket = false, .type = SOCK_STREAM, + .abstract = false, };FIXTURE_SETUP (outside_socket) {drop_caps (_metadata); -memset (&self->transit_address,0 ,sizeof (self->transit_address)); -set_unix_address (&self->transit_address,0 , true); -memset (&self->address,0 ,sizeof (self->address)); -set_unix_address (&self->address,1 , true); +ASSERT_EQ (0 ,mkdir (PATHNAME_UNIX_SOCK_DIR ,0700 )); + +/* Abstract addresses. */ +memset (&self->transit_address_abstract,0 , +sizeof (self->transit_address_abstract)); +set_unix_address (&self->transit_address_abstract,0 , true); +memset (&self->address_abstract,0 ,sizeof (self->address_abstract)); +set_unix_address (&self->address_abstract,1 , true); + +/* Pathname addresses. */ +memset (&self->transit_address_pathname,0 , +sizeof (self->transit_address_pathname)); +set_unix_address (&self->transit_address_pathname,2 , false); +memset (&self->address_pathname,0 ,sizeof (self->address_pathname)); +set_unix_address (&self->address_pathname,3 , false); }FIXTURE_TEARDOWN (outside_socket) { +EXPECT_EQ (0 ,remove_path (self->transit_address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,remove_path (self->address_pathname.unix_addr.sun_path)); +EXPECT_EQ (0 ,rmdir (PATHNAME_UNIX_SOCK_DIR )); }/* @@ -798,6 +892,15 @@ TEST_F(outside_socket, socket_with_different_domain)int pipe_child[2 ], pipe_parent[2 ];char buf_parent;int server_socket; +const __u16 scope = variant->abstract ? +LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET : +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ; +const struct service_fixture *transit_address = + variant->abstract ? &self->transit_address_abstract : + &self->transit_address_pathname; +const struct service_fixture *address = + variant->abstract ? &self->address_abstract : + &self->address_pathname;ASSERT_EQ (0 ,pipe2 (pipe_child,O_CLOEXEC ));ASSERT_EQ (0 ,pipe2 (pipe_parent,O_CLOEXEC ));@@ -812,8 +915,7 @@ TEST_F(outside_socket, socket_with_different_domain)EXPECT_EQ (0 ,close (pipe_child[0 ]));/* Client always has a domain. */ -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope);if (variant->child_socket) {int data_socket, passed_socket, stream_server;@@ -823,8 +925,8 @@ TEST_F(outside_socket, socket_with_different_domain) stream_server =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_server);ASSERT_EQ (0 ,bind (stream_server, - &self->transit_address.unix_addr, - self->transit_address.unix_addr_len)); + &transit_address->unix_addr, + transit_address->unix_addr_len));ASSERT_EQ (0 ,listen (stream_server, backlog));ASSERT_EQ (1 ,write (pipe_child[1 ],"." ,1 )); data_socket =accept (stream_server,NULL ,NULL );@@ -839,8 +941,8 @@ TEST_F(outside_socket, socket_with_different_domain)/* Waits for parent signal for connection. */ ASSERT_EQ (1 ,read (pipe_parent[0 ], &buf_child,1 )); - err =connect (client_socket, &self->address.unix_addr, - self->address.unix_addr_len); + err =connect (client_socket, &address->unix_addr, + address->unix_addr_len);if (variant->child_socket) {EXPECT_EQ (0 , err); }else {@@ -859,9 +961,8 @@ TEST_F(outside_socket, socket_with_different_domain)ASSERT_LE (0 , client_child);ASSERT_EQ (1 ,read (pipe_child[0 ], &buf_parent,1 )); -ASSERT_EQ (0 ,connect (client_child, - &self->transit_address.unix_addr, - self->transit_address.unix_addr_len)); +ASSERT_EQ (0 ,connect (client_child, &transit_address->unix_addr, + transit_address->unix_addr_len)); server_socket =recv_fd (client_child);EXPECT_EQ (0 ,close (client_child)); }else {@@ -870,10 +971,10 @@ TEST_F(outside_socket, socket_with_different_domain)ASSERT_LE (0 , server_socket);/* Server always has a domain. */ -create_scoped_domain (_metadata,LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope); -ASSERT_EQ (0 ,bind (server_socket, &self->address.unix_addr, - self->address.unix_addr_len)); +ASSERT_EQ (0 , +bind (server_socket, &address->unix_addr, address->unix_addr_len));if (variant->type ==SOCK_STREAM )ASSERT_EQ (0 ,listen (server_socket, backlog));@@ -888,52 +989,85 @@ TEST_F(outside_socket, socket_with_different_domain) _metadata->exit_code =KSFT_FAIL ; } -static const char stream_path[] =TMP_DIR "/stream.sock" ; -static const char dgram_path[] =TMP_DIR "/dgram.sock" ; -/* clang-format off */ -FIXTURE (various_address_sockets) {}; +FIXTURE (various_address_sockets) { +struct service_fixture stream_pathname_addr, dgram_pathname_addr; +struct service_fixture stream_abstract_addr, dgram_abstract_addr; +};/* clang-format on */ -FIXTURE_VARIANT (various_address_sockets) -{ -const int domain; +/* +* Test all 4 combinations of abstract and pathname socket scope bits, +* plus a case with no Landlock domain at all. +*/ +/* clang-format off */ +FIXTURE_VARIANT (various_address_sockets) { +/* clang-format on */ +const __u16 scope_bits; +const bool no_sandbox; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(various_address_sockets, scope_abstract) { +/* clang-format on */ + .scope_bits =LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET , +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(various_address_sockets, scope_pathname) { +/* clang-format on */ + .scope_bits =LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET , };/* clang-format off */ -FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_scoped_domain) { +FIXTURE_VARIANT_ADD(various_address_sockets, scope_both) {/* clang-format on */ - .domain =SCOPE_SANDBOX , + .scope_bits =LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET , };/* clang-format off */ -FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_other_domain) { +FIXTURE_VARIANT_ADD(various_address_sockets, scope_none) {/* clang-format on */ - .domain =OTHER_SANDBOX , + .scope_bits = 0, };/* clang-format off */ -FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_no_domain) { +FIXTURE_VARIANT_ADD(various_address_sockets, no_domain) {/* clang-format on */ - .domain = NO_SANDBOX, + .no_sandbox = true, };FIXTURE_SETUP (various_address_sockets) {drop_caps (_metadata); -umask (0077 ); -ASSERT_EQ (0 ,mkdir (TMP_DIR ,0700 )); +ASSERT_EQ (0 ,mkdir (PATHNAME_UNIX_SOCK_DIR ,0700 )); + +memset (&self->stream_pathname_addr,0 ,sizeof (self->stream_pathname_addr)); +set_unix_address (&self->stream_pathname_addr,0 , false); +memset (&self->dgram_pathname_addr,0 ,sizeof (self->dgram_pathname_addr)); +set_unix_address (&self->dgram_pathname_addr,1 , false); + +memset (&self->stream_abstract_addr,0 ,sizeof (self->stream_abstract_addr)); +set_unix_address (&self->stream_abstract_addr,2 , true); +memset (&self->dgram_abstract_addr,0 ,sizeof (self->dgram_abstract_addr)); +set_unix_address (&self->dgram_abstract_addr,3 , true); }FIXTURE_TEARDOWN (various_address_sockets) { -EXPECT_EQ (0 ,unlink (stream_path)); -EXPECT_EQ (0 ,unlink (dgram_path)); -EXPECT_EQ (0 ,rmdir (TMP_DIR )); +EXPECT_EQ (0 ,remove_path (self->stream_pathname_addr.unix_addr.sun_path)); +EXPECT_EQ (0 ,remove_path (self->dgram_pathname_addr.unix_addr.sun_path)); +EXPECT_EQ (0 ,rmdir (PATHNAME_UNIX_SOCK_DIR )); } -TEST_F(various_address_sockets, scoped_pathname_sockets) +/* +* Test interaction of various scope flags (controlled by variant->domain) +* with pathname and abstract sockets when connecting from a sandboxed +* child. +*/ +TEST_F(various_address_sockets, scoped_sockets) {pid_t child;int status;@@ -942,25 +1076,10 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)int unnamed_sockets[2 ];int stream_pathname_socket, dgram_pathname_socket, stream_abstract_socket, dgram_abstract_socket, data_socket; -struct service_fixture stream_abstract_addr, dgram_abstract_addr; -struct sockaddr_un stream_pathname_addr = { - .sun_family =AF_UNIX , - }; -struct sockaddr_un dgram_pathname_addr = { - .sun_family =AF_UNIX , - }; - -/* Pathname address. */ -snprintf (stream_pathname_addr.sun_path, -sizeof (stream_pathname_addr.sun_path),"%s" , stream_path); -snprintf (dgram_pathname_addr.sun_path, -sizeof (dgram_pathname_addr.sun_path),"%s" , dgram_path); - -/* Abstract address. */ -memset (&stream_abstract_addr,0 ,sizeof (stream_abstract_addr)); -set_unix_address (&stream_abstract_addr,0 , true); -memset (&dgram_abstract_addr,0 ,sizeof (dgram_abstract_addr)); -set_unix_address (&dgram_abstract_addr,1 , true); +bool pathname_restricted = + (variant->scope_bits &LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ); +bool abstract_restricted = + (variant->scope_bits &LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET );/* Unnamed address for datagram socket. */ ASSERT_EQ (0 ,socketpair (AF_UNIX ,SOCK_DGRAM ,0 , unnamed_sockets));@@ -975,82 +1094,103 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)EXPECT_EQ (0 ,close (pipe_parent[1 ]));EXPECT_EQ (0 ,close (unnamed_sockets[1 ])); -if (variant->domain ==SCOPE_SANDBOX ) -create_scoped_domain ( - _metadata,LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); -else if (variant->domain ==OTHER_SANDBOX ) +/* Create domain based on variant. */ +if (variant->scope_bits)
tools/testing/selftests/landlock/scoped_unix_test.c TEST_F(various_address_sockets, scoped_pathname_sockets) · line 1107
Quoted 10 lines
tools/testing/selftests/landlock/scoped_unix_test.c +3
-1+create_scoped_domain (_metadata, variant->scope_bits); + else if (!variant->no_sandbox)create_fs_domain (_metadata);/* Waits for parent to listen. */ ASSERT_EQ (1 ,read (pipe_parent[0 ], &buf_child,1 ));EXPECT_EQ (0 ,close (pipe_parent[0 ])); -/* Checks that we can send data through a datagram socket. */ +/* Checks that we can send data through a unnamed socket. */
an unnamed
Quoted 413 lines
tools/testing/selftests/landlock/scoped_unix_test.c +161
-89ASSERT_EQ (1 ,write (unnamed_sockets[0 ],"a" ,1 ));EXPECT_EQ (0 ,close (unnamed_sockets[0 ]));/* Connects with pathname sockets. */ stream_pathname_socket =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_pathname_socket); -ASSERT_EQ (0 , -connect (stream_pathname_socket, &stream_pathname_addr, -sizeof (stream_pathname_addr))); -ASSERT_EQ (1 ,write (stream_pathname_socket,"b" ,1 )); + err =connect (stream_pathname_socket, + &self->stream_pathname_addr.unix_addr, + self->stream_pathname_addr.unix_addr_len); +if (pathname_restricted) { +EXPECT_EQ (-1 , err); +EXPECT_EQ (EPERM , errno); + }else { +EXPECT_EQ (0 , err); +ASSERT_EQ (1 ,write (stream_pathname_socket,"b" ,1 )); + }EXPECT_EQ (0 ,close (stream_pathname_socket)); -/* Sends without connection. */ +/* Sends without connection (pathname). */ dgram_pathname_socket =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , dgram_pathname_socket); err =sendto (dgram_pathname_socket,"c" ,1 ,0 , - &dgram_pathname_addr,sizeof (dgram_pathname_addr)); -EXPECT_EQ (1 , err); + &self->dgram_pathname_addr.unix_addr, + self->dgram_pathname_addr.unix_addr_len); +if (pathname_restricted) { +EXPECT_EQ (-1 , err); +EXPECT_EQ (EPERM , errno); + }else { +EXPECT_EQ (1 , err); + } + +/* Sends with connection (pathname). */ + err =connect (dgram_pathname_socket, + &self->dgram_pathname_addr.unix_addr, + self->dgram_pathname_addr.unix_addr_len); +if (pathname_restricted) { +EXPECT_EQ (-1 , err); +EXPECT_EQ (EPERM , errno); + }else { +EXPECT_EQ (0 , err); +ASSERT_EQ (1 ,write (dgram_pathname_socket,"d" ,1 )); + } -/* Sends with connection. */ -ASSERT_EQ (0 , -connect (dgram_pathname_socket, &dgram_pathname_addr, -sizeof (dgram_pathname_addr))); -ASSERT_EQ (1 ,write (dgram_pathname_socket,"d" ,1 ));EXPECT_EQ (0 ,close (dgram_pathname_socket));/* Connects with abstract sockets. */ stream_abstract_socket =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_abstract_socket); err =connect (stream_abstract_socket, - &stream_abstract_addr.unix_addr, - stream_abstract_addr.unix_addr_len); -if (variant->domain ==SCOPE_SANDBOX ) { + &self->stream_abstract_addr.unix_addr, + self->stream_abstract_addr.unix_addr_len); +if (abstract_restricted) {EXPECT_EQ (-1 , err);EXPECT_EQ (EPERM , errno); }else {EXPECT_EQ (0 , err);ASSERT_EQ (1 ,write (stream_abstract_socket,"e" ,1 )); } +EXPECT_EQ (0 ,close (stream_abstract_socket)); -/* Sends without connection. */ +/* Sends without connection (abstract). */ dgram_abstract_socket =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , dgram_abstract_socket); err =sendto (dgram_abstract_socket,"f" ,1 ,0 , - &dgram_abstract_addr.unix_addr, - dgram_abstract_addr.unix_addr_len); -if (variant->domain ==SCOPE_SANDBOX ) { + &self->dgram_abstract_addr.unix_addr, + self->dgram_abstract_addr.unix_addr_len); +if (abstract_restricted) {EXPECT_EQ (-1 , err);EXPECT_EQ (EPERM , errno); }else {EXPECT_EQ (1 , err); } -/* Sends with connection. */ +/* Sends with connection (abstract). */ err =connect (dgram_abstract_socket, - &dgram_abstract_addr.unix_addr, - dgram_abstract_addr.unix_addr_len); -if (variant->domain ==SCOPE_SANDBOX ) { + &self->dgram_abstract_addr.unix_addr, + self->dgram_abstract_addr.unix_addr_len); +if (abstract_restricted) {EXPECT_EQ (-1 , err);EXPECT_EQ (EPERM , errno); }else {EXPECT_EQ (0 , err);ASSERT_EQ (1 ,write (dgram_abstract_socket,"g" ,1 )); } +EXPECT_EQ (0 ,close (dgram_abstract_socket));_exit (_metadata->exit_code);@@ -1062,27 +1202,30 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)/* Sets up pathname servers. */ stream_pathname_socket =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_pathname_socket); -ASSERT_EQ (0 ,bind (stream_pathname_socket, &stream_pathname_addr, -sizeof (stream_pathname_addr))); +ASSERT_EQ (0 ,bind (stream_pathname_socket, + &self->stream_pathname_addr.unix_addr, + self->stream_pathname_addr.unix_addr_len));ASSERT_EQ (0 ,listen (stream_pathname_socket, backlog)); dgram_pathname_socket =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , dgram_pathname_socket); -ASSERT_EQ (0 ,bind (dgram_pathname_socket, &dgram_pathname_addr, -sizeof (dgram_pathname_addr))); +ASSERT_EQ (0 ,bind (dgram_pathname_socket, + &self->dgram_pathname_addr.unix_addr, + self->dgram_pathname_addr.unix_addr_len));/* Sets up abstract servers. */ stream_abstract_socket =socket (AF_UNIX ,SOCK_STREAM ,0 );ASSERT_LE (0 , stream_abstract_socket); -ASSERT_EQ (0 , -bind (stream_abstract_socket, &stream_abstract_addr.unix_addr, - stream_abstract_addr.unix_addr_len)); +ASSERT_EQ (0 ,bind (stream_abstract_socket, + &self->stream_abstract_addr.unix_addr, + self->stream_abstract_addr.unix_addr_len)); +ASSERT_EQ (0 ,listen (stream_abstract_socket, backlog)); dgram_abstract_socket =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , dgram_abstract_socket); -ASSERT_EQ (0 ,bind (dgram_abstract_socket, &dgram_abstract_addr.unix_addr, - dgram_abstract_addr.unix_addr_len)); -ASSERT_EQ (0 ,listen (stream_abstract_socket, backlog)); +ASSERT_EQ (0 ,bind (dgram_abstract_socket, + &self->dgram_abstract_addr.unix_addr, + self->dgram_abstract_addr.unix_addr_len));ASSERT_EQ (1 ,write (pipe_parent[1 ],"." ,1 ));EXPECT_EQ (0 ,close (pipe_parent[1 ]));@@ -1092,24 +1235,31 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)ASSERT_EQ ('a' , buf_parent);EXPECT_LE (0 ,close (unnamed_sockets[1 ])); -/* Reads from pathname sockets. */ - data_socket =accept (stream_pathname_socket,NULL ,NULL ); -ASSERT_LE (0 , data_socket); -ASSERT_EQ (1 ,read (data_socket, &buf_parent,sizeof (buf_parent))); -ASSERT_EQ ('b' , buf_parent); -EXPECT_EQ (0 ,close (data_socket)); -EXPECT_EQ (0 ,close (stream_pathname_socket)); +if (!pathname_restricted) { +/* +* Reads from pathname sockets if we expect child to be able to +* send. +*/ + data_socket =accept (stream_pathname_socket,NULL ,NULL ); +ASSERT_LE (0 , data_socket); +ASSERT_EQ (1 , +read (data_socket, &buf_parent,sizeof (buf_parent))); +ASSERT_EQ ('b' , buf_parent); +EXPECT_EQ (0 ,close (data_socket)); -ASSERT_EQ (1 , -read (dgram_pathname_socket, &buf_parent,sizeof (buf_parent))); -ASSERT_EQ ('c' , buf_parent); -ASSERT_EQ (1 , -read (dgram_pathname_socket, &buf_parent,sizeof (buf_parent))); -ASSERT_EQ ('d' , buf_parent); -EXPECT_EQ (0 ,close (dgram_pathname_socket)); +ASSERT_EQ (1 ,read (dgram_pathname_socket, &buf_parent, +sizeof (buf_parent))); +ASSERT_EQ ('c' , buf_parent); +ASSERT_EQ (1 ,read (dgram_pathname_socket, &buf_parent, +sizeof (buf_parent))); +ASSERT_EQ ('d' , buf_parent); + } -if (variant->domain !=SCOPE_SANDBOX ) { -/* Reads from abstract sockets if allowed to send. */ +if (!abstract_restricted) { +/* +* Reads from abstract sockets if we expect child to be able to +* send. +*/ data_socket =accept (stream_abstract_socket,NULL ,NULL );ASSERT_LE (0 , data_socket); ASSERT_EQ(1,@@ -1125,30 +1275,73 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)ASSERT_EQ ('g' , buf_parent); } -/* Waits for all abstract socket tests. */ +/* Waits for child to complete, and only close the socket afterwards. */ ASSERT_EQ (child,waitpid (child, &status,0 ));EXPECT_EQ (0 ,close (stream_abstract_socket));EXPECT_EQ (0 ,close (dgram_abstract_socket)); +EXPECT_EQ (0 ,close (stream_pathname_socket)); +EXPECT_EQ (0 ,close (dgram_pathname_socket));if (WIFSIGNALED (status) || !WIFEXITED (status) ||WEXITSTATUS (status) !=EXIT_SUCCESS ) _metadata->exit_code =KSFT_FAIL ; } -TEST (datagram_sockets) +/* Fixture for datagram_sockets and self_connect tests */ +FIXTURE (socket_type_test) {struct service_fixture connected_addr, non_connected_addr; +}; + +FIXTURE_VARIANT (socket_type_test) +{ +const bool abstract; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(socket_type_test, abstract) { +/* clang-format on */ + .abstract = true, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(socket_type_test, pathname) { +/* clang-format on */ + .abstract = false, +}; + +FIXTURE_SETUP (socket_type_test) +{ +drop_caps (_metadata); + +if (!variant->abstract) +ASSERT_EQ (0 ,mkdir (PATHNAME_UNIX_SOCK_DIR ,0700 )); + +memset (&self->connected_addr,0 ,sizeof (self->connected_addr)); +set_unix_address (&self->connected_addr,0 , variant->abstract); +memset (&self->non_connected_addr,0 ,sizeof (self->non_connected_addr)); +set_unix_address (&self->non_connected_addr,1 , variant->abstract); +} + +FIXTURE_TEARDOWN (socket_type_test) +{ +if (!variant->abstract) { +EXPECT_EQ (0 ,remove_path (self->connected_addr.unix_addr.sun_path)); +EXPECT_EQ (0 ,remove_path (self->non_connected_addr.unix_addr.sun_path)); +EXPECT_EQ (0 ,rmdir (PATHNAME_UNIX_SOCK_DIR )); + } +} + +TEST_F(socket_type_test, datagram_sockets) +{int server_conn_socket, server_unconn_socket;int pipe_parent[2 ], pipe_child[2 ];int status;char buf;pid_t child; - -drop_caps (_metadata); -memset (&connected_addr,0 ,sizeof (connected_addr)); -set_unix_address (&connected_addr,0 , true); -memset (&non_connected_addr,0 ,sizeof (non_connected_addr)); -set_unix_address (&non_connected_addr,1 , true); +const __u16 scope = variant->abstract ? +LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET : +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ;ASSERT_EQ (0 ,pipe2 (pipe_parent,O_CLOEXEC ));ASSERT_EQ (0 ,pipe2 (pipe_child,O_CLOEXEC ));@@ -1169,8 +1362,9 @@ TEST(datagram_sockets)/* Waits for parent to listen. */ ASSERT_EQ (1 ,read (pipe_parent[0 ], &buf,1 ));ASSERT_EQ (0 , -connect (client_conn_socket, &connected_addr.unix_addr, - connected_addr.unix_addr_len)); +connect (client_conn_socket, + &self->connected_addr.unix_addr, + self->connected_addr.unix_addr_len));/* * Both connected and non-connected sockets can send data when @@ -1178,13 +1372,12 @@ TEST(datagram_sockets)*/ ASSERT_EQ (1 ,send (client_conn_socket,"." ,1 ,0 ));ASSERT_EQ (1 ,sendto (client_unconn_socket,"." ,1 ,0 , - &non_connected_addr.unix_addr, - non_connected_addr.unix_addr_len)); + &self->non_connected_addr.unix_addr, + self->non_connected_addr.unix_addr_len));ASSERT_EQ (1 ,write (pipe_child[1 ],"." ,1 ));/* Scopes the domain. */ -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope);/* * Connected socket sends data to the receiver, but the @@ -1192,8 +1385,8 @@ TEST(datagram_sockets)*/ ASSERT_EQ (1 ,send (client_conn_socket,"." ,1 ,0 ));ASSERT_EQ (-1 ,sendto (client_unconn_socket,"." ,1 ,0 , - &non_connected_addr.unix_addr, - non_connected_addr.unix_addr_len)); + &self->non_connected_addr.unix_addr, + self->non_connected_addr.unix_addr_len));ASSERT_EQ (EPERM , errno);ASSERT_EQ (1 ,write (pipe_child[1 ],"." ,1 ));@@ -1210,10 +1403,11 @@ TEST(datagram_sockets)ASSERT_LE (0 , server_conn_socket);ASSERT_LE (0 , server_unconn_socket); -ASSERT_EQ (0 ,bind (server_conn_socket, &connected_addr.unix_addr, - connected_addr.unix_addr_len)); -ASSERT_EQ (0 ,bind (server_unconn_socket, &non_connected_addr.unix_addr, - non_connected_addr.unix_addr_len)); +ASSERT_EQ (0 ,bind (server_conn_socket, &self->connected_addr.unix_addr, + self->connected_addr.unix_addr_len)); +ASSERT_EQ (0 ,bind (server_unconn_socket, + &self->non_connected_addr.unix_addr, + self->non_connected_addr.unix_addr_len));ASSERT_EQ (1 ,write (pipe_parent[1 ],"." ,1 ));/* Waits for child to test. */ @@ -1238,52 +1432,49 @@ TEST(datagram_sockets) _metadata->exit_code =KSFT_FAIL ; } -TEST (self_connect) +TEST_F(socket_type_test, self_connect) { -struct service_fixture connected_addr, non_connected_addr;int connected_socket, non_connected_socket, status;pid_t child; - -drop_caps (_metadata); -memset (&connected_addr,0 ,sizeof (connected_addr)); -set_unix_address (&connected_addr,0 , true); -memset (&non_connected_addr,0 ,sizeof (non_connected_addr)); -set_unix_address (&non_connected_addr,1 , true); +const __u16 scope = variant->abstract ? +LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET : +LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET ; connected_socket =socket (AF_UNIX ,SOCK_DGRAM ,0 ); non_connected_socket =socket (AF_UNIX ,SOCK_DGRAM ,0 );ASSERT_LE (0 , connected_socket);ASSERT_LE (0 , non_connected_socket); -ASSERT_EQ (0 ,bind (connected_socket, &connected_addr.unix_addr, - connected_addr.unix_addr_len)); -ASSERT_EQ (0 ,bind (non_connected_socket, &non_connected_addr.unix_addr, - non_connected_addr.unix_addr_len)); +ASSERT_EQ (0 ,bind (connected_socket, &self->connected_addr.unix_addr, + self->connected_addr.unix_addr_len)); +ASSERT_EQ (0 ,bind (non_connected_socket, + &self->non_connected_addr.unix_addr, + self->non_connected_addr.unix_addr_len)); child =fork ();ASSERT_LE (0 , child);if (child ==0 ) {/* Child's domain is scoped. */ -create_scoped_domain (_metadata, -LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ); +create_scoped_domain (_metadata, scope);/* * The child inherits the sockets, and cannot connect or * send data to them. */ ASSERT_EQ (-1 , -connect (connected_socket, &connected_addr.unix_addr, - connected_addr.unix_addr_len)); +connect (connected_socket, + &self->connected_addr.unix_addr, + self->connected_addr.unix_addr_len));ASSERT_EQ (EPERM , errno);ASSERT_EQ (-1 ,sendto (connected_socket,"." ,1 ,0 , - &connected_addr.unix_addr, - connected_addr.unix_addr_len)); + &self->connected_addr.unix_addr, + self->connected_addr.unix_addr_len));ASSERT_EQ (EPERM , errno);ASSERT_EQ (-1 ,sendto (non_connected_socket,"." ,1 ,0 , - &non_connected_addr.unix_addr, - non_connected_addr.unix_addr_len)); + &self->non_connected_addr.unix_addr, + self->non_connected_addr.unix_addr_len));ASSERT_EQ (EPERM , errno);EXPECT_EQ (0 ,close (connected_socket));Signature · 1 line
2.52.0
Hello!
Thanks for sending this patch!
Quoted 54 lines · Tingmao Wang
Changes in v2:
Fix grammar in doc, rebased on mic/next, and extracted common code fromhook_unix_stream_connectandhook_unix_may_sendinto a separate function.The rest is the same as the v1 cover letter:
This patch series extend the existing abstract Unix socket scoping to pathname (i.e. normal file-based) sockets as well, by adding a new scope bit
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETthat works the same asLANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, except that restricts pathname Unix sockets. This means that a sandboxed process with this scope enabled will not be able to connect to Unix sockets created outside the sandbox via the filesystem.There is a future plan [1] for allowing specific sockets based on FS hierarchy, but this series is only determining access based on domain parent-child relationship. There is currently no way to allow specific (outside the Landlock domain) Unix sockets, and none of the existing Landlock filesystem controls apply to socket
connect().With this series, we can now properly protect against things like the the following while only relying on Landlock:
(running under tmux) root@6-19-0-rc1-dev-00023-g68f0b276cbeb ~# LL_FS_RO=/ LL_FS_RW= ./sandboxer bash Executing the sandboxed command... root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi cat: /tmp/hi: No such file or directory root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# tmux new-window 'echo hi > /tmp/hi' root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi hiThe above but with Unix socket scoping enabled (both pathname and abstract sockets) - the sandboxed shell can now no longer talk to tmux due to the socket being created from outside the Landlock sandbox:
(running under tmux)
root@6-19-0-rc1-dev-00023-g68f0b276cbeb ~# LL_FS_RO=/ LL_FS_RW= LL_SCOPED=u:a ./sandboxer bash Executing the sandboxed command...
root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat/tmp/hi
cat:/tmp/hi: No such file or directory root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# tmux new-window 'echo hi >/tmp/hi' error connecting to/tmp/tmux-0/default(Operation not permitted) root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat/tmp/hi
cat:/tmp/hi: No such file or directoryTmux is just one example. In a standard systemd session,
systemd-run --usercan also be used (--user will run the command in the user's session, without requiring any root privileges), and likely a lot more if running in a desktop environment with many popular applications. This change therefore makes it possible to create sandboxes without relying on additional mechanisms like seccomp to protect against such issues.
Quoted 3 lines
These kind of issues was originally discussed on here (I took the idea for systemd-run from Demi):
https://spectrum-os.org/lists/archives/spectrum-devel/[email protected]/
What is unclear to me from the examples and the description is: Why is the boundary between allowed and denied connection targets drawn at the border of the Landlock domain (the "scope") and why don't we solve this with the file-system-based approach described in [1]?
**Do we have existing use cases where a service is both offered and connected to all from within the same Landlock domain, and where the process enforcing the policy does not control the child process enough so that it would be possible to allow-list it with a LANDLOCK_ACCESS_FS_CONNECT_UNIX rule?**
If we do not have such a use case, it seems that the planned FS-based control mechanism from [1] would do the same job? Long term, we might be better off if we only have only one control -- as we have discussed in [2], having two of these might mean that they interact in unconventional and possibly confusing ways.
Apart from that, there are some other weaker hints that make me slightly critical of this patch set:
* We discussed the idea that a FS-based path_beneath rule would act implicitly also as an exception for LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET, in [2] - one possible way to interpret this is that the gravity of the system's logic pulls us back towards a FS-based control, and we would have to swim less against the stream if we integrated the Unix connect() control in that way?
* I am struggling to convince myself that we can tell users to restrict LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET as a default, as we are currently doing it with the other "scoped" and file system controls. (The scoped signals are OK because killing out-of-domain processes is clearly bad. The scoped abstract sockets are usually OK because most processes do not need that feature.)
But there are legitimate Unix services that are still needed by unprivileged processes and which are designed to be safe to use. For instance, systemd exposes a user database lookup service over varlink [3], which can be accessed from arbitrary glibc programs through a NSS module. Using this is incompatible with LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET as long as we do not have the FS-based control and the surprising implicit permission through a path_beneath rule as discussed in [2].
(Another example is the X11 socket, to which the same reasoning should apply, I think, and which is also used by a large number of programs.)
I agree that the bug [1] has been asleep for a bit too long, and we should probably pick this up soon. As we have not heard back from Ryan after our last inquiry, IMHO I think it would be fair to take it over.
Apologies for the difficult feedback - I do not mean to get in the way of your enthusiasm here, but I would like to make sure that we don't implement this as a stop-gap measure just because the other bug [1] seemed more difficult and/or stuck in a github issue interaction. Let's rather unblock that bug, if that is the case. :)
As usual, I fully acknowledge that I might well be wrong and might have missed some of the underlying reasons, in which case I will happily be corrected and change my mind. :)
[1] https://github.com/landlock-lsm/linux/issues/36
[2] https://github.com/landlock-lsm/linux/issues/36#issuecomment-3699749541
[3] https://systemd.io/USER_GROUP_API/
Have a good start into the new year!
–Günther
Quoted 74 lines · Günther Noack
Hello!
Thanks for sending this patch!
Quoted 54 lines · Tingmao Wang
Changes in v2:
Fix grammar in doc, rebased on mic/next, and extracted common code fromhook_unix_stream_connectandhook_unix_may_sendinto a separate function.The rest is the same as the v1 cover letter:
This patch series extend the existing abstract Unix socket scoping to pathname (i.e. normal file-based) sockets as well, by adding a new scope bit
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETthat works the same asLANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, except that restricts pathname Unix sockets. This means that a sandboxed process with this scope enabled will not be able to connect to Unix sockets created outside the sandbox via the filesystem.There is a future plan [1] for allowing specific sockets based on FS hierarchy, but this series is only determining access based on domain parent-child relationship. There is currently no way to allow specific (outside the Landlock domain) Unix sockets, and none of the existing Landlock filesystem controls apply to socket
connect().With this series, we can now properly protect against things like the the following while only relying on Landlock:
(running under tmux) root@6-19-0-rc1-dev-00023-g68f0b276cbeb ~# LL_FS_RO=/ LL_FS_RW= ./sandboxer bash Executing the sandboxed command... root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi cat: /tmp/hi: No such file or directory root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# tmux new-window 'echo hi > /tmp/hi' root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi hiThe above but with Unix socket scoping enabled (both pathname and abstract sockets) - the sandboxed shell can now no longer talk to tmux due to the socket being created from outside the Landlock sandbox:
(running under tmux)
root@6-19-0-rc1-dev-00023-g68f0b276cbeb ~# LL_FS_RO=/ LL_FS_RW= LL_SCOPED=u:a ./sandboxer bash Executing the sandboxed command...
root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat/tmp/hi
cat:/tmp/hi: No such file or directory root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# tmux new-window 'echo hi >/tmp/hi' error connecting to/tmp/tmux-0/default(Operation not permitted) root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat/tmp/hi
cat:/tmp/hi: No such file or directoryTmux is just one example. In a standard systemd session,
systemd-run --usercan also be used (--user will run the command in the user's session, without requiring any root privileges), and likely a lot more if running in a desktop environment with many popular applications. This change therefore makes it possible to create sandboxes without relying on additional mechanisms like seccomp to protect against such issues.Quoted 3 lines
These kind of issues was originally discussed on here (I took the idea for systemd-run from Demi):
https://spectrum-os.org/lists/archives/spectrum-devel/[email protected]/What is unclear to me from the examples and the description is: Why is the boundary between allowed and denied connection targets drawn at the border of the Landlock domain (the "scope") and why don't we solve this with the file-system-based approach described in [1]?
**Do we have existing use cases where a service is both offered and connected to all from within the same Landlock domain, and where the process enforcing the policy does not control the child process enough so that it would be possible to allow-list it with a
LANDLOCK_ACCESS_FS_CONNECT_UNIXrule?**
Quoted 5 lines
If we do not have such a use case, it seems that the planned FS-based control mechanism from [1] would do the same job? Long term, we might be better off if we only have only one control -- as we have discussed in [2], having two of these might mean that they interact in unconventional and possibly confusing ways.
I agree with this.
Quoted 10 lines
Apart from that, there are some other weaker hints that make me slightly critical of this patch set:
* We discussed the idea that a FS-based
path_beneathrule would act implicitly also as an exception forLANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET, in [2] - one possible way to interpret this is that the gravity of the system's logic pulls us back towards a FS-based control, and we would have to swim less against the stream if we integrated the Unixconnect()control in that way?
I agree with this as well. Having FS-based controls apply consistently to all types of inodes is what I would expect.
Quoted 16 lines
* I am struggling to convince myself that we can tell users to restrict
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETas a default, as we are currently doing it with the other "scoped" and file system controls. (The scoped signals are OK because killing out-of-domain processes is clearly bad. The scoped abstract sockets are usually OK because most processes do not need that feature.)But there are legitimate Unix services that are still needed by unprivileged processes and which are designed to be safe to use. For instance, systemd exposes a user database lookup service over varlink [3], which can be accessed from arbitrary glibc programs through a NSS module. Using this is incompatible with
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETas long as we do not have the FS-based control and the surprising implicit permission through apath_beneathrule as discussed in [2].
Quoted 3 lines
(Another example is the X11 socket, to which the same reasoning should apply, I think, and which is also used by a large number of programs.)
I think making FS-based controls on by default is the least surprising option for users. It's what I suspect most programs intend, and it would stop a lot of unexpected sandbox escapes.
Without FS-based controls, scoping is also necessary to prevent sandbox escapes. In my opinion, it's better to block access to unprivileged services than to allow connecting to any service.
X11 is a trivial sandbox escape by taking over the user's GUI. A sandboxed program accessing it indicates a misdesign. The NSS module is not needed by programs that do not do user database resolution, which I suspect includes almost all sandboxed programs.
Quoted 4 lines
I agree that the bug [1] has been asleep for a bit too long, and we should probably pick this up soon. As we have not heard back from Ryan after our last inquiry, IMHO I think it would be fair to take it over.
I definitely support this!
Quoted 16 lines
Apologies for the difficult feedback - I do not mean to get in the way of your enthusiasm here, but I would like to make sure that we don't implement this as a stop-gap measure just because the other bug [1] seemed more difficult and/or stuck in a github issue interaction. Let's rather unblock that bug, if that is the case. :)
As usual, I fully acknowledge that I might well be wrong and might have missed some of the underlying reasons, in which case I will happily be corrected and change my mind. :)
[1] https://github.com/landlock-lsm/linux/issues/36
[2] https://github.com/landlock-lsm/linux/issues/36#issuecomment-3699749541
[3] https://systemd.io/USER_GROUP_API/Have a good start into the new year!
–Günther
Signature · 2 lines
Sincerely, Demi Marie Obenour (she/her/hers)
Mickaël Salaün
maintainerQuoted 69 lines · Demi Marie Obenour
Quoted 68 lines · Günther Noack
Hello!
Thanks for sending this patch!
Quoted 54 lines · Tingmao Wang
Changes in v2:
Fix grammar in doc, rebased on mic/next, and extracted common code fromhook_unix_stream_connectandhook_unix_may_sendinto a separate function.The rest is the same as the v1 cover letter:
This patch series extend the existing abstract Unix socket scoping to pathname (i.e. normal file-based) sockets as well, by adding a new scope bit
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETthat works the same asLANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, except that restricts pathname Unix sockets. This means that a sandboxed process with this scope enabled will not be able to connect to Unix sockets created outside the sandbox via the filesystem.There is a future plan [1] for allowing specific sockets based on FS hierarchy, but this series is only determining access based on domain parent-child relationship. There is currently no way to allow specific (outside the Landlock domain) Unix sockets, and none of the existing Landlock filesystem controls apply to socket
connect().With this series, we can now properly protect against things like the the following while only relying on Landlock:
(running under tmux) root@6-19-0-rc1-dev-00023-g68f0b276cbeb ~# LL_FS_RO=/ LL_FS_RW= ./sandboxer bash Executing the sandboxed command... root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi cat: /tmp/hi: No such file or directory root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# tmux new-window 'echo hi > /tmp/hi' root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat /tmp/hi hiThe above but with Unix socket scoping enabled (both pathname and abstract sockets) - the sandboxed shell can now no longer talk to tmux due to the socket being created from outside the Landlock sandbox:
(running under tmux)
root@6-19-0-rc1-dev-00023-g68f0b276cbeb ~# LL_FS_RO=/ LL_FS_RW= LL_SCOPED=u:a ./sandboxer bash Executing the sandboxed command...
root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat/tmp/hi
cat:/tmp/hi: No such file or directory root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# tmux new-window 'echo hi >/tmp/hi' error connecting to/tmp/tmux-0/default(Operation not permitted) root@6-19-0-rc1-dev-00023-g68f0b276cbeb:/# cat/tmp/hi
cat:/tmp/hi: No such file or directoryTmux is just one example. In a standard systemd session,
systemd-run --usercan also be used (--user will run the command in the user's session, without requiring any root privileges), and likely a lot more if running in a desktop environment with many popular applications. This change therefore makes it possible to create sandboxes without relying on additional mechanisms like seccomp to protect against such issues.Quoted 3 lines
These kind of issues was originally discussed on here (I took the idea for systemd-run from Demi):
https://spectrum-os.org/lists/archives/spectrum-devel/[email protected]/What is unclear to me from the examples and the description is: Why is the boundary between allowed and denied connection targets drawn at the border of the Landlock domain (the "scope") and why don't we solve this with the file-system-based approach described in [1]?
Quoted 5 lines
Quoted 5 lines
**Do we have existing use cases where a service is both offered and connected to all from within the same Landlock domain, and where the process enforcing the policy does not control the child process enough so that it would be possible to allow-list it with a
LANDLOCK_ACCESS_FS_CONNECT_UNIXrule?**
Yes, with the sandboxer use case. It's similar to a container that doesn't know all programs that could be run in it. We should be able to create sandboxes that don't assume (nor restrict) internal IPCs (or any kind of direct process I/O).
Landlock should make it possible to scope any kind of IPC. It's a mental model which is easy to understand and that should be enforced by default. Of course, we need some ways to add exceptions to this deny-by-default policy, and that's why we also need the FS-based control mechanism.
Quoted 8 lines
Quoted 6 lines
If we do not have such a use case, it seems that the planned FS-based control mechanism from [1] would do the same job? Long term, we might be better off if we only have only one control -- as we have discussed in [2], having two of these might mean that they interact in unconventional and possibly confusing ways.
I agree with this.
Both approaches are complementary/orthogonal and make sense long term too. This might be the first time we can restrict the same operations, but I'm pretty sure it will not be the last, and it's OK. Handled FS access and scoped restrictions are two ways to describe a part of the security policy. Having different ways makes the interface much simpler than a generic one that would have to take into account all potential future access controls.
One thing to keep in mind is that UAPI doesn't have to map 1:1 to the kernel implementation, but the UAPI is stable and future proof, whereas the kernel implementation can change a lot.
The ruleset's handled fields serve two purposes: define what should be denied by default, and define which type of rules are valid (compatibility). The ruleset's scoped field serve similar purposes but it also implies implicit rules (i.e. communications with processes inside the sandbox are allowed). Without the soped field, we would have to create dedicated handled field per type (i.e. scope's bit) and dedicated rule type for the related handled field, which would make the interface more generic but also more complex, for something which is not needed.
In a nutshell, in the case of the FS-based and scope-based unix socket control, we should see one kind of restrictions (e.g. connect to unix socket), which can accept two types of rules: (explicit) file path, or (implicit) peer's scope. Access should be granted as long as a rule matches, whatever its type.
This rationale should be explained in a commit message.
Quoted 12 lines
Quoted 11 lines
Apart from that, there are some other weaker hints that make me slightly critical of this patch set:
* We discussed the idea that a FS-based
path_beneathrule would act implicitly also as an exception forLANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET, in [2] - one possible way to interpret this is that the gravity of the system's logic pulls us back towards a FS-based control, and we would have to swim less against the stream if we integrated the Unixconnect()control in that way?
Quoted 2 lines
I agree with this as well. Having FS-based controls apply consistently to all types of inodes is what I would expect.
I'm not sure to understand, but I hope my explanation above will answer this question.
Quoted 17 lines
Quoted 17 lines
* I am struggling to convince myself that we can tell users to restrict
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETas a default, as we are currently doing it with the other "scoped" and file system controls. (The scoped signals are OK because killing out-of-domain processes is clearly bad. The scoped abstract sockets are usually OK because most processes do not need that feature.)But there are legitimate Unix services that are still needed by unprivileged processes and which are designed to be safe to use. For instance, systemd exposes a user database lookup service over varlink [3], which can be accessed from arbitrary glibc programs through a NSS module. Using this is incompatible with
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETas long as we do not have the FS-based control and the surprising implicit permission through apath_beneathrule as discussed in [2].
Quoted 3 lines
Quoted 3 lines
(Another example is the X11 socket, to which the same reasoning should apply, I think, and which is also used by a large number of programs.)
Yes, restricting a sandbox too much may not work for any possible use cases. Some use cases would need the FS-based control before using the scope-based one, and that's OK. Some use cases would not need the FS-based control, and the scope-based one would be enough (e.g. sandboxed archive manager).
Quoted 3 lines
I think making FS-based controls on by default is the least surprising option for users. It's what I suspect most programs intend, and it would stop a lot of unexpected sandbox escapes.
Nothing in Landlock is on by default for compatibility reasons.
Quoted 4 lines
Without FS-based controls, scoping is also necessary to prevent sandbox escapes. In my opinion, it's better to block access to unprivileged services than to allow connecting to any service.
Scoped-based control should be enforced in most cases, but FS-based control might be a requirement/dependency. In any case, a sandbox should not break legitimate use cases.
Quoted 3 lines
X11 is a trivial sandbox escape by taking over the user's GUI. A sandboxed program accessing it indicates a misdesign.
I would say a *fully* sandboxed program for a desktop use case using X11 (instead of Wayland), which is not possible with Landlock *alone* yet. Desktop environment is the worse case scenario for sandboxing, for a lot of different reasons... but there are a lot of other use cases for sandboxing.
Quoted 10 lines
The NSS module
is not needed by programs that do not do user database resolution, which I suspect includes almost all sandboxed programs.Quoted 4 lines
I agree that the bug [1] has been asleep for a bit too long, and we should probably pick this up soon. As we have not heard back from Ryan after our last inquiry, IMHO I think it would be fair to take it over.
I definitely support this!
Yes, thanks for working on this.
Quoted 6 lines
Quoted 6 lines
Apologies for the difficult feedback - I do not mean to get in the way of your enthusiasm here, but I would like to make sure that we don't implement this as a stop-gap measure just because the other bug [1] seemed more difficult and/or stuck in a github issue interaction. Let's rather unblock that bug, if that is the case. :)
Receiving criticisms is part of the review process, which is a sign of healthy development. I understand your concerns and it's good discuss about them to make sure we are going in the right direction.
Quoted 4 lines
Quoted 4 lines
As usual, I fully acknowledge that I might well be wrong and might have missed some of the underlying reasons, in which case I will happily be corrected and change my mind. :)
In any case, if someone doesn't understand something, it probably means that this should be explained better somewhere. Such design artifacts will be useful for future developments too.
Quoted 10 lines
Quoted 7 lines
[1] https://github.com/landlock-lsm/linux/issues/36
[2] https://github.com/landlock-lsm/linux/issues/36#issuecomment-3699749541
[3] https://systemd.io/USER_GROUP_API/Have a good start into the new year!
–GüntherSignature · 2 lines
Sincerely, Demi Marie Obenour (she/her/hers)
Quoted 59 lines · Mickaël Salaün
Quoted 10 lines · Demi Marie Obenour
Quoted 9 lines · Günther Noack
[...]
Quoted 1 line · Tingmao Wang
[...]
What is unclear to me from the examples and the description is: Why is the boundary between allowed and denied connection targets drawn at the border of the Landlock domain (the "scope") and why don't we solve this with the file-system-based approach described in [1]?
Quoted 5 lines
Quoted 5 lines
**Do we have existing use cases where a service is both offered and connected to all from within the same Landlock domain, and where the process enforcing the policy does not control the child process enough so that it would be possible to allow-list it with a
LANDLOCK_ACCESS_FS_CONNECT_UNIXrule?**Yes, with the sandboxer use case. It's similar to a container that doesn't know all programs that could be run in it. We should be able to create sandboxes that don't assume (nor restrict) internal IPCs (or any kind of direct process I/O).
Landlock should make it possible to scope any kind of IPC. It's a mental model which is easy to understand and that should be enforced by default. Of course, we need some ways to add exceptions to this deny-by-default policy, and that's why we also need the FS-based control mechanism.
Quoted 8 lines
Quoted 6 lines
If we do not have such a use case, it seems that the planned FS-based control mechanism from [1] would do the same job? Long term, we might be better off if we only have only one control -- as we have discussed in [2], having two of these might mean that they interact in unconventional and possibly confusing ways.
I agree with this.
Both approaches are complementary/orthogonal and make sense long term too. This might be the first time we can restrict the same operations, but I'm pretty sure it will not be the last, and it's OK. Handled FS access and scoped restrictions are two ways to describe a part of the security policy. Having different ways makes the interface much simpler than a generic one that would have to take into account all potential future access controls.
One thing to keep in mind is that UAPI doesn't have to map 1:1 to the kernel implementation, but the UAPI is stable and future proof, whereas the kernel implementation can change a lot.
The ruleset's handled fields serve two purposes: define what should be denied by default, and define which type of rules are valid (compatibility). The ruleset's scoped field serve similar purposes but it also implies implicit rules (i.e. communications with processes inside the sandbox are allowed). Without the soped field, we would have to create dedicated handled field per type (i.e. scope's bit) and dedicated rule type for the related handled field, which would make the interface more generic but also more complex, for something which is not needed.
Quoted 19 lines
In a nutshell, in the case of the FS-based and scope-based unix socket control, we should see one kind of restrictions (e.g. connect to unix socket), which can accept two types of rules: (explicit) file path, or (implicit) peer's scope. Access should be granted as long as a rule matches, whatever its type.
This rationale should be explained in a commit message.
Quoted 11 lines
Quoted 11 lines
Apart from that, there are some other weaker hints that make me slightly critical of this patch set:
* We discussed the idea that a FS-based
path_beneathrule would act implicitly also as an exception forLANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET, in [2] - one possible way to interpret this is that the gravity of the system's logic pulls us back towards a FS-based control, and we would have to swim less against the stream if we integrated the Unixconnect()control in that way?
While I do think being able to control UNIX socket access via domain scoping is useful (after all, it is an additional bit of information / relationship that "normal" filesystem objects doesn't have, and applications using Landlock might find it useful to control access based on this extra information, either to simplify the policy, or because a generic sandboxer can't come up with a pre-determined list of UNIX socket fs rules, e.g. for applications which places socket in hard-coded "/tmp"), I do share the worry about how this might get confusing from an API perspective, as discussed in my GitHub comment [1].
Another way to put it is that, if FS-based and scope-based controls interacts in the above proposed way, both mechanisms feel like "poking holes" in the other. But as Mickaël said, one can think of the two mechanisms not as independent controls, but rather as two interfaces for the same control. The socket access control is "enabled" if either the LANDLOCK_ACCESS_FS_RESOLVE_UNIX access is handled, or the scope bit proposed in this patch is enabled.
With that said, I can think of some alternative ways that might make this API look "better" (from a subjective point of view, feedback welcome), however it does mean more delays, and specifically, these will depend on LANDLOCK_ACCESS_FS_RESOLVE_UNIX:
One possibility is to simply always allow a Landlock domain to connect to its own sockets (in the case where LANDLOCK_ACCESS_FS_RESOLVE_UNIX is handled, otherwise all sockets are allowed). This might be reasonable, as one can only connect to a socket it creates if it has the permission to create it in the first place, which is already controlled by LANDLOCK_ACCESS_FS_MAKE_SOCK, so we don't really lose any policy flexibility here - if for some reason the sandboxer don't want to allow access to any (pathname) sockets, even the sandboxed app's own ones, it can just not allow LANDLOCK_ACCESS_FS_MAKE_SOCK anywhere.
Another possibility is to have this scope control as part of the LANDLOCK_ACCESS_FS_RESOLVE_UNIX rule itself, rather than as a separate scope bit, via introducing a "rule flag" (which is a new mechanism proposed in [2]) which I will tentatively call (LANDLOCK_ADD_RULE_)SAME_SCOPE_ONLY. So for example:
- If the sandboxer wants to allow all socket access, don't handle LANDLOCK_ACCESS_FS_RESOLVE_UNIX at all, or handle it but have an allow rule at / with no flags.
- If it wants to allow access to only the sandboxed app's own sockets, handle LANDLOCK_ACCESS_FS_RESOLVE_UNIX, then place one allow rule on / with the rule flag SAME_SCOPE_ONLY. This means that the allow rule "UNIX socket under /" only allows connecting to sockets in the same domain scope. Additional rules without this rule flag can be placed on more specific places to add "exceptions" to allow access to more privileged sockets.
- To allow access to specific sockets only, not even the sandboxed app's own sockets, just use LANDLOCK_ACCESS_FS_RESOLVE_UNIX without this new rule flag.
This is slightly more flexible than just the LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET scope bit plus LANDLOCK_ACCESS_FS_RESOLVE_UNIX, as it allows the sandboxer to specify where an application can connect, even with sockets in its own domain, and it also nicely avoids the "what feels like two independent controls poking holes in each other" problem. But it does mean more complexity (although hopefully not too much), as we now need to introduce the rule flags concept, but that is required for some other proposed stuff anyway - quiet flags [3] and no_inherit [4] rules, and the rule flags design is in a good state I think.
What do folks think?
(btw, when I say "sandboxed app" this can of course also mean multiple applications in the same sandbox, e.g. like a container runtime as Mickaël pointed out, which raises the probability that just having LANDLOCK_ACCESS_FS_RESOLVE_UNIX would be insufficient, e.g. when bind mounts are involved, or other cases which I haven't thought of right now)
[1]: https://github.com/landlock-lsm/linux/issues/36#issuecomment-3693123942
[2]: https://lore.kernel.org/all/f238931bc813fc50fc8e11a007a8ad2136024df3.1766330134.git.m@maowtm.org/
[3]: https://lore.kernel.org/all/[email protected]/
[4]: https://lore.kernel.org/all/[email protected]/
Mickaël Salaün
maintainerQuoted 103 lines · Tingmao Wang
Quoted 59 lines · Mickaël Salaün
Quoted 10 lines · Demi Marie Obenour
Quoted 9 lines · Günther Noack
[...]
Quoted 1 line · Tingmao Wang
[...]
What is unclear to me from the examples and the description is: Why is the boundary between allowed and denied connection targets drawn at the border of the Landlock domain (the "scope") and why don't we solve this with the file-system-based approach described in [1]?
Quoted 5 lines
Quoted 5 lines
**Do we have existing use cases where a service is both offered and connected to all from within the same Landlock domain, and where the process enforcing the policy does not control the child process enough so that it would be possible to allow-list it with a
LANDLOCK_ACCESS_FS_CONNECT_UNIXrule?**Yes, with the sandboxer use case. It's similar to a container that doesn't know all programs that could be run in it. We should be able to create sandboxes that don't assume (nor restrict) internal IPCs (or any kind of direct process I/O).
Landlock should make it possible to scope any kind of IPC. It's a mental model which is easy to understand and that should be enforced by default. Of course, we need some ways to add exceptions to this deny-by-default policy, and that's why we also need the FS-based control mechanism.
Quoted 8 lines
Quoted 6 lines
If we do not have such a use case, it seems that the planned FS-based control mechanism from [1] would do the same job? Long term, we might be better off if we only have only one control -- as we have discussed in [2], having two of these might mean that they interact in unconventional and possibly confusing ways.
I agree with this.
Both approaches are complementary/orthogonal and make sense long term too. This might be the first time we can restrict the same operations, but I'm pretty sure it will not be the last, and it's OK. Handled FS access and scoped restrictions are two ways to describe a part of the security policy. Having different ways makes the interface much simpler than a generic one that would have to take into account all potential future access controls.
One thing to keep in mind is that UAPI doesn't have to map 1:1 to the kernel implementation, but the UAPI is stable and future proof, whereas the kernel implementation can change a lot.
The ruleset's handled fields serve two purposes: define what should be denied by default, and define which type of rules are valid (compatibility). The ruleset's scoped field serve similar purposes but it also implies implicit rules (i.e. communications with processes inside the sandbox are allowed). Without the soped field, we would have to create dedicated handled field per type (i.e. scope's bit) and dedicated rule type for the related handled field, which would make the interface more generic but also more complex, for something which is not needed.
Quoted 19 lines
In a nutshell, in the case of the FS-based and scope-based unix socket control, we should see one kind of restrictions (e.g. connect to unix socket), which can accept two types of rules: (explicit) file path, or (implicit) peer's scope. Access should be granted as long as a rule matches, whatever its type.
This rationale should be explained in a commit message.
Quoted 11 lines
Quoted 11 lines
Apart from that, there are some other weaker hints that make me slightly critical of this patch set:
* We discussed the idea that a FS-based
path_beneathrule would act implicitly also as an exception forLANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET, in [2] - one possible way to interpret this is that the gravity of the system's logic pulls us back towards a FS-based control, and we would have to swim less against the stream if we integrated the Unixconnect()control in that way?While I do think being able to control UNIX socket access via domain scoping is useful (after all, it is an additional bit of information / relationship that "normal" filesystem objects doesn't have, and applications using Landlock might find it useful to control access based on this extra information, either to simplify the policy, or because a generic sandboxer can't come up with a pre-determined list of UNIX socket fs rules, e.g. for applications which places socket in hard-coded "/tmp"), I do share the worry about how this might get confusing from an API perspective, as discussed in my GitHub comment [1].
Another way to put it is that, if FS-based and scope-based controls interacts in the above proposed way, both mechanisms feel like "poking holes" in the other. But as Mickaël said, one can think of the two mechanisms not as independent controls, but rather as two interfaces for the same control. The socket access control is "enabled" if either the
LANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess is handled, or the scope bit proposed in this patch is enabled.With that said, I can think of some alternative ways that might make this API look "better" (from a subjective point of view, feedback welcome), however it does mean more delays, and specifically, these will depend on
LANDLOCK_ACCESS_FS_RESOLVE_UNIX:
Quoted 9 lines
One possibility is to simply always allow a Landlock domain to connect to its own sockets (in the case where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled, otherwise all sockets are allowed). This might be reasonable, as one can only connect to a socket it creates if it has the permission to create it in the first place, which is already controlled byLANDLOCK_ACCESS_FS_MAKE_SOCK, so we don't really lose any policy flexibility here - if for some reason the sandboxer don't want to allow access to any (pathname) sockets, even the sandboxed app's own ones, it can just not allowLANDLOCK_ACCESS_FS_MAKE_SOCKanywhere.
LANDLOCK_ACCESS_FS_MAKE_SOCK is only required to bind/listen to a socket, not to connect. I guess you was thinking about LANDLOCK_ACCESS_FS_RESOLVE_UNIX in this case?
Quoted 23 lines
Another possibility is to have this scope control as part of the
LANDLOCK_ACCESS_FS_RESOLVE_UNIXrule itself, rather than as a separate scope bit, via introducing a "rule flag" (which is a new mechanism proposed in [2]) which I will tentatively call (LANDLOCK_ADD_RULE_)SAME_SCOPE_ONLY. So for example:- If the sandboxer wants to allow all socket access, don't handle
LANDLOCK_ACCESS_FS_RESOLVE_UNIXat all, or handle it but have an allow rule at / with no flags.- If it wants to allow access to only the sandboxed app's own sockets, handle
LANDLOCK_ACCESS_FS_RESOLVE_UNIX, then place one allow rule on / with the rule flagSAME_SCOPE_ONLY. This means that the allow rule "UNIX socket under /" only allows connecting to sockets in the same domain scope. Additional rules without this rule flag can be placed on more specific places to add "exceptions" to allow access to more privileged sockets.- To allow access to specific sockets only, not even the sandboxed app's own sockets, just use
LANDLOCK_ACCESS_FS_RESOLVE_UNIXwithout this new rule flag.
Quoted 9 lines
This is slightly more flexible than just the
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETscope bit plusLANDLOCK_ACCESS_FS_RESOLVE_UNIX, as it allows the sandboxer to specify where an application can connect, even with sockets in its own domain, and it also nicely avoids the "what feels like two independent controls poking holes in each other" problem. But it does mean more complexity (although hopefully not too much), as we now need to introduce the rule flags concept, but that is required for some other proposed stuff anyway - quiet flags [3] andno_inherit[4] rules, and the rule flags design is in a good state I think.
This looks too complex and would intertwine both concept, which is more confusing IMO.
Quoted 2 lines
What do folks think?
I'd like to keep a clean API, with a "scoped" field handling IPC scoping, and an "handled_access_fs" field handling filesystem-related accesses.
One thing to keep in mind is that we could add a new kind of "handled" field that would enable to add rules identifying e.g. processes, cgroups, or Landlock domains, and that could be used to add exceptions to the current scopes. This means that we need to have a generic way to handle this case.
What is the issue with two complementary interfaces (scope and access) used to express a policy about connecting to UNIX sockets? We just need to make sure that scopes and handled_access_fs dealing with UNIX sockets are like binary OR: if the scope is set, then the domain can communicate with peers which are in the same domain, and if the handled_access_fs right is set, then the domain can only communicate with matching sockets (OR scoped ones if the scope is set).
My main concern is about user space libraries and users that may want to have conditional enforcement for compatibility reasons e.g., only enforce LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET (let's say ABI v8) if it can also set LANDLOCK_ACCESS_FS_RESOLVE_UNIX (let's say ABI v9). I see two ways to deal with this case (if needed):
- add synthetic access right to easily let users "combine" two access rigths or none;
- have a more generic way to AND and OR access rights. I'm thinking about updating the Rust library in this direction.
Anyway, we need to decide if this should be merged in Linux 7.0 (next week) or not. I'd prefer to merge it now because I think it works well and it's not a new concept wrt the abstract UNIX socket scoping. However, if there are any concern, I'd like to hear them now and I can delay this merge if needed. This patch series still need a new version but that should only be about cosmetic fixes. WDYT?
Quoted 12 lines
(btw, when I say "sandboxed app" this can of course also mean multiple applications in the same sandbox, e.g. like a container runtime as Mickaël pointed out, which raises the probability that just having
LANDLOCK_ACCESS_FS_RESOLVE_UNIXwould be insufficient, e.g. when bind mounts are involved, or other cases which I haven't thought of right now)[1]: https://github.com/landlock-lsm/linux/issues/36#issuecomment-3693123942
[2]: https://lore.kernel.org/all/f238931bc813fc50fc8e11a007a8ad2136024df3.1766330134.git.m@maowtm.org/
[3]: https://lore.kernel.org/all/[email protected]/
[4]: https://lore.kernel.org/all/[email protected]/
Quoted 173 lines · Mickaël Salaün
Quoted 96 lines · Tingmao Wang
Quoted 54 lines · Mickaël Salaün
Quoted 9 lines · Demi Marie Obenour
Quoted 8 lines · Günther Noack
[...]
Quoted 1 line · Tingmao Wang
[...]
What is unclear to me from the examples and the description is: Why is the boundary between allowed and denied connection targets drawn at the border of the Landlock domain (the "scope") and why don't we solve this with the file-system-based approach described in [1]?
Quoted 5 lines
Quoted 5 lines
**Do we have existing use cases where a service is both offered and connected to all from within the same Landlock domain, and where the process enforcing the policy does not control the child process enough so that it would be possible to allow-list it with a
LANDLOCK_ACCESS_FS_CONNECT_UNIXrule?**Yes, with the sandboxer use case. It's similar to a container that doesn't know all programs that could be run in it. We should be able to create sandboxes that don't assume (nor restrict) internal IPCs (or any kind of direct process I/O).
Landlock should make it possible to scope any kind of IPC. It's a mental model which is easy to understand and that should be enforced by default. Of course, we need some ways to add exceptions to this deny-by-default policy, and that's why we also need the FS-based control mechanism.
Quoted 6 lines
Quoted 5 lines
If we do not have such a use case, it seems that the planned FS-based control mechanism from [1] would do the same job? Long term, we might be better off if we only have only one control -- as we have discussed in [2], having two of these might mean that they interact in unconventional and possibly confusing ways.
I agree with this.
Both approaches are complementary/orthogonal and make sense long term too. This might be the first time we can restrict the same operations, but I'm pretty sure it will not be the last, and it's OK. Handled FS access and scoped restrictions are two ways to describe a part of the security policy. Having different ways makes the interface much simpler than a generic one that would have to take into account all potential future access controls.
One thing to keep in mind is that UAPI doesn't have to map 1:1 to the kernel implementation, but the UAPI is stable and future proof, whereas the kernel implementation can change a lot.
The ruleset's handled fields serve two purposes: define what should be denied by default, and define which type of rules are valid (compatibility). The ruleset's scoped field serve similar purposes but it also implies implicit rules (i.e. communications with processes inside the sandbox are allowed). Without the soped field, we would have to create dedicated handled field per type (i.e. scope's bit) and dedicated rule type for the related handled field, which would make the interface more generic but also more complex, for something which is not needed.
Quoted 18 lines
In a nutshell, in the case of the FS-based and scope-based unix socket control, we should see one kind of restrictions (e.g. connect to unix socket), which can accept two types of rules: (explicit) file path, or (implicit) peer's scope. Access should be granted as long as a rule matches, whatever its type.
This rationale should be explained in a commit message.
Quoted 10 lines
Quoted 10 lines
Apart from that, there are some other weaker hints that make me slightly critical of this patch set:
* We discussed the idea that a FS-based
path_beneathrule would act implicitly also as an exception forLANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET, in [2] - one possible way to interpret this is that the gravity of the system's logic pulls us back towards a FS-based control, and we would have to swim less against the stream if we integrated the Unixconnect()control in that way?While I do think being able to control UNIX socket access via domain scoping is useful (after all, it is an additional bit of information / relationship that "normal" filesystem objects doesn't have, and applications using Landlock might find it useful to control access based on this extra information, either to simplify the policy, or because a generic sandboxer can't come up with a pre-determined list of UNIX socket fs rules, e.g. for applications which places socket in hard-coded "/tmp"), I do share the worry about how this might get confusing from an API perspective, as discussed in my GitHub comment [1].
Another way to put it is that, if FS-based and scope-based controls interacts in the above proposed way, both mechanisms feel like "poking holes" in the other. But as Mickaël said, one can think of the two mechanisms not as independent controls, but rather as two interfaces for the same control. The socket access control is "enabled" if either the
LANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess is handled, or the scope bit proposed in this patch is enabled.With that said, I can think of some alternative ways that might make this API look "better" (from a subjective point of view, feedback welcome), however it does mean more delays, and specifically, these will depend on
LANDLOCK_ACCESS_FS_RESOLVE_UNIX:Quoted 9 lines
One possibility is to simply always allow a Landlock domain to connect to its own sockets (in the case where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled, otherwise all sockets are allowed). This might be reasonable, as one can only connect to a socket it creates if it has the permission to create it in the first place, which is already controlled byLANDLOCK_ACCESS_FS_MAKE_SOCK, so we don't really lose any policy flexibility here - if for some reason the sandboxer don't want to allow access to any (pathname) sockets, even the sandboxed app's own ones, it can just not allowLANDLOCK_ACCESS_FS_MAKE_SOCKanywhere.
LANDLOCK_ACCESS_FS_MAKE_SOCKis only required to bind/listen to a socket, not to connect. I guess you was thinking aboutLANDLOCK_ACCESS_FS_RESOLVE_UNIXin this case?Quoted 22 lines
Another possibility is to have this scope control as part of the
LANDLOCK_ACCESS_FS_RESOLVE_UNIXrule itself, rather than as a separate scope bit, via introducing a "rule flag" (which is a new mechanism proposed in [2]) which I will tentatively call (LANDLOCK_ADD_RULE_)SAME_SCOPE_ONLY. So for example:- If the sandboxer wants to allow all socket access, don't handle
LANDLOCK_ACCESS_FS_RESOLVE_UNIXat all, or handle it but have an allow rule at / with no flags.- If it wants to allow access to only the sandboxed app's own sockets, handle
LANDLOCK_ACCESS_FS_RESOLVE_UNIX, then place one allow rule on / with the rule flagSAME_SCOPE_ONLY. This means that the allow rule "UNIX socket under /" only allows connecting to sockets in the same domain scope. Additional rules without this rule flag can be placed on more specific places to add "exceptions" to allow access to more privileged sockets.- To allow access to specific sockets only, not even the sandboxed app's own sockets, just use
LANDLOCK_ACCESS_FS_RESOLVE_UNIXwithout this new rule flag.Quoted 9 lines
This is slightly more flexible than just the
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETscope bit plusLANDLOCK_ACCESS_FS_RESOLVE_UNIX, as it allows the sandboxer to specify where an application can connect, even with sockets in its own domain, and it also nicely avoids the "what feels like two independent controls poking holes in each other" problem. But it does mean more complexity (although hopefully not too much), as we now need to introduce the rule flags concept, but that is required for some other proposed stuff anyway - quiet flags [3] andno_inherit[4] rules, and the rule flags design is in a good state I think.This looks too complex and would intertwine both concept, which is more confusing IMO.
Quoted 1 line
What do folks think?
I'd like to keep a clean API, with a "scoped" field handling IPC scoping, and an "
handled_access_fs" field handling filesystem-related accesses.One thing to keep in mind is that we could add a new kind of "handled" field that would enable to add rules identifying e.g. processes, cgroups, or Landlock domains, and that could be used to add exceptions to the current scopes. This means that we need to have a generic way to handle this case.
What is the issue with two complementary interfaces (scope and access) used to express a policy about connecting to UNIX sockets? We just need to make sure that scopes and
handled_access_fsdealing with UNIX sockets are like binary OR: if the scope is set, then the domain can communicate with peers which are in the same domain, and if thehandled_access_fsright is set, then the domain can only communicate with matching sockets (OR scoped ones if the scope is set).My main concern is about user space libraries and users that may want to have conditional enforcement for compatibility reasons e.g., only enforce
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET(let's say ABI v8) if it can also setLANDLOCK_ACCESS_FS_RESOLVE_UNIX(let's say ABI v9). I see two ways to deal with this case (if needed):
- add synthetic access right to easily let users "combine" two access rigths or none;
- have a more generic way to AND and OR access rights. I'm thinking about updating the Rust library in this direction.
Quoted 6 lines
Anyway, we need to decide if this should be merged in Linux 7.0 (next week) or not. I'd prefer to merge it now because I think it works well and it's not a new concept wrt the abstract UNIX socket scoping. However, if there are any concern, I'd like to hear them now and I can delay this merge if needed. This patch series still need a new version but that should only be about cosmetic fixes. WDYT?
Regardless if you merge the patch series now in 7.0 or a later version, I think there is something to be said about having the filesystem and scoped unix access right merged in the same ABI version / merge window.
As you pointed out earlier, the combination of the two flags is much flexible and useful to userspace consumers than one or the other, and if the features were merged separately, there would be an awkward middle ABI where user space consumers may have to make compromises or changes to sandbox between different versions or change application behavior.
I think the discussion has died down on the security_unix_find hook for LANDLOCK_ACCESS_FS_RESOLVE_UNIX [1] [2], and it seemed the general consensus was favorable.
The series for LANDLOCK_ACCESS_FS_RESOLVE_UNIX is pretty small, and feedback on the last version was pretty quiet as well. So maybe it's worth a final look into. [3]
(PS: I do think if they are merged together, there should be tests specifically for the combination of the two access rights. I'd be happy to draft some)
Justin
[1] : https://lore.kernel.org/linux-security-module/[email protected]/
[2] : https://lore.kernel.org/linux-security-module/[email protected]/
[3] : https://lore.kernel.org/linux-security-module/[email protected]/
Quoted 12 lines
Quoted 12 lines
(btw, when I say "sandboxed app" this can of course also mean multiple applications in the same sandbox, e.g. like a container runtime as Mickaël pointed out, which raises the probability that just having
LANDLOCK_ACCESS_FS_RESOLVE_UNIXwould be insufficient, e.g. when bind mounts are involved, or other cases which I haven't thought of right now)[1]: https://github.com/landlock-lsm/linux/issues/36#issuecomment-3693123942
[2]: https://lore.kernel.org/all/f238931bc813fc50fc8e11a007a8ad2136024df3.1766330134.git.m@maowtm.org/
[3]: https://lore.kernel.org/all/[email protected]/
[4]: https://lore.kernel.org/all/[email protected]/
Hi Mickaël,
Thanks for the feedback and explanations :)
Quoted 14 lines · Mickaël Salaün
Quoted 2 lines · Tingmao Wang
[...]
What do folks think?I'd like to keep a clean API, with a "scoped" field handling IPC scoping, and an "
handled_access_fs" field handling filesystem-related accesses.One thing to keep in mind is that we could add a new kind of "handled" field that would enable to add rules identifying e.g. processes, cgroups, or Landlock domains, and that could be used to add exceptions to the current scopes. This means that we need to have a generic way to handle this case.
Quoted 7 lines
What is the issue with two complementary interfaces (scope and access) used to express a policy about connecting to UNIX sockets? We just need to make sure that scopes and
handled_access_fsdealing with UNIX sockets are like binary OR: if the scope is set, then the domain can communicate with peers which are in the same domain, and if thehandled_access_fsright is set, then the domain can only communicate with matching sockets (OR scoped ones if the scope is set).
Right, I see what you're saying, especially with the "additional access rules for other scopes" example, and I think I'm happy with this. I guess my attempt at trying to make the API more "elegant" would introduce complexity and also create future inconsistency if other existing scope bits are combined with handled_access rules.
Quoted 7 lines
[...]
Anyway, we need to decide if this should be merged in Linux 7.0 (next week) or not. I'd prefer to merge it now because I think it works well and it's not a new concept wrt the abstract UNIX socket scoping. However, if there are any concern, I'd like to hear them now and I can delay this merge if needed. This patch series still need a new version but that should only be about cosmetic fixes. WDYT?
I ended up being pretty busy today but I can definitely send the next version tomorrow with your formatting changes and comments. I'm happy with it going into the next merge window if you are. Justin raises a point about having these two mechanisms in the same ABI version - see below for consideration.
Quoted 11 lines
Quoted 1 line
[...]
My main concern is about user space libraries and users that may want to have conditional enforcement for compatibility reasons e.g., only enforce
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET(let's say ABI v8) if it can also setLANDLOCK_ACCESS_FS_RESOLVE_UNIX(let's say ABI v9). I see two ways to deal with this case (if needed):
- add synthetic access right to easily let users "combine" two access rigths or none;
- have a more generic way to AND and OR access rights. I'm thinking about updating the Rust library in this direction.
I'm not sure I fully understand the complexity here, but I think, assuming these land in separate kernel versions, it will have to be that if both the scope bit and LANDLOCK_ACCESS_FS_RESOLVE_UNIX is requested (maybe if the user actually adds rules containing RESOLVE_UNIX access), but only the scope bit is supported, then it will have to skip enforcing pathname UNIX socket restrictions altogether by skipping both the scope bit and the RESOLVE_UNIX access (if in best effort mode), or fail (if in hard requirement mode).
I don't immediately see how further customization ability (e.g. synthetic access rights or other AND/OR combination) could be used - if an app needs access to a privileged socket and can't pre-open it before landlock_restrict_self(), then the only realistic choice is to not use the scope bits if LANDLOCK_ACCESS_FS_RESOLVE_UNIX is not supported.
Quoted 8 lines · Justin Suess
Regardless if you merge the patch series now in 7.0 or a later version, I think there is something to be said about having the filesystem and scoped unix access right merged in the same ABI version / merge window.
As you pointed out earlier, the combination of the two flags is much flexible and useful to userspace consumers than one or the other, and if the features were merged separately, there would be an awkward middle ABI where user space consumers may have to make compromises or changes to sandbox between different versions or change application behavior.
[...]
Given that the scope bit and RESOLVE_UNIX access right are in some sense part of the same system (they interact in an OR manner, after all), there is some positive for having them introduced in the same version, but on the other hand, with my above reasoning, I don't think these two mechanisms (scope bit and RESOLVE_UNIX access) being in different ABI versions would be too much of a problem. In either case, for applications which require access to more "privileged" sockets, when running on a kernel without the RESOLVE_UNIX access right support, no pathname socket restrictions can be applied (i.e. it won't use the scope bit either, there isn't much "compromise" it can make here). On the other hand, if RESOLVE_UNIX is supported, then it knows that the scope bit is also supported, and can just use it.
Furthermore, an application / Landlock config etc can always opt to not use the scope bit at all, if it "knows" all the locations where the application's sockets would be placed, and just use RESOLVE_UNIX access right (or nothing if it is not supported).
(The following is a bit of a side note, not terribly relevant if we're deciding to go with the patch as is.)
Quoted 24 lines
Quoted 14 lines
[...]
Another way to put it is that, if FS-based and scope-based controls interacts in the above proposed way, both mechanisms feel like "poking holes" in the other. But as Mickaël said, one can think of the two mechanisms not as independent controls, but rather as two interfaces for the same control. The socket access control is "enabled" if either theLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess is handled, or the scope bit proposed in this patch is enabled.With that said, I can think of some alternative ways that might make this API look "better" (from a subjective point of view, feedback welcome), however it does mean more delays, and specifically, these will depend on
LANDLOCK_ACCESS_FS_RESOLVE_UNIX:Quoted 9 lines
One possibility is to simply always allow a Landlock domain to connect to its own sockets (in the case where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled, otherwise all sockets are allowed). This might be reasonable, as one can only connect to a socket it creates if it has the permission to create it in the first place, which is already controlled byLANDLOCK_ACCESS_FS_MAKE_SOCK, so we don't really lose any policy flexibility here - if for some reason the sandboxer don't want to allow access to any (pathname) sockets, even the sandboxed app's own ones, it can just not allowLANDLOCK_ACCESS_FS_MAKE_SOCKanywhere.
Quoted 3 lines
LANDLOCK_ACCESS_FS_MAKE_SOCKis only required to bind/listen to a socket, not to connect. I guess you was thinking aboutLANDLOCK_ACCESS_FS_RESOLVE_UNIXin this case?
In this "allow same-scope connect unconditionally" proposal, the application would still be able to (bind to and) connect to its own sockets, even if LANDLOCK_ACCESS_FS_RESOLVE_UNIX is handled and nothing is allowed to have LANDLOCK_ACCESS_FS_RESOLVE_UNIX access. But a sandboxer which for whatever reason doesn't want this "allow same scope" default can still prevent the use of (pathname) sockets by restricting LANDLOCK_ACCESS_FS_MAKE_SOCK, because if an app can't connect to any sockets it doesn't own, and can't create any sockets itself either, then it effectively can't connect to any sockets at all.
(Although on second thought, I guess there could be a case where an app first creates some socket files before doing landlock_restrict_self(), then it might still be able to bind to these even without LANDLOCK_ACCESS_FS_MAKE_SOCK?)
Günther Noack
reviewerQuoted 10 lines · Tingmao Wang
Quoted 8 lines · Justin Suess
Regardless if you merge the patch series now in 7.0 or a later version, I think there is something to be said about having the filesystem and scoped unix access right merged in the same ABI version / merge window.
As you pointed out earlier, the combination of the two flags is much flexible and useful to userspace consumers than one or the other, and if the features were merged separately, there would be an awkward middle ABI where user space consumers may have to make compromises or changes to sandbox between different versions or change application behavior.
[...]
Quoted 12 lines
Given that the scope bit and
RESOLVE_UNIXaccess right are in some sense part of the same system (they interact in an OR manner, after all), there is some positive for having them introduced in the same version, but on the other hand, with my above reasoning, I don't think these two mechanisms (scope bit andRESOLVE_UNIXaccess) being in different ABI versions would be too much of a problem. In either case, for applications which require access to more "privileged" sockets, when running on a kernel without theRESOLVE_UNIXaccess right support, no pathname socket restrictions can be applied (i.e. it won't use the scope bit either, there isn't much "compromise" it can make here). On the other hand, ifRESOLVE_UNIXis supported, then it knows that the scope bit is also supported, and can just use it.
Yes, but that does require additional subtle backwards compatibility logic in userspace libraries, to implement the "best effort" fallbacks.
Assuming the scoped bit is added in v8 and the FS_RESOLVE_UNIX right in v9, if a user does this (in Go-landlock syntax):
// restrict both scoped bit and FS RESOLVE_UNIX right, if possible landlock.V9.BestEffort().RestrictPaths(
landlock.ResolveUnix("/tmp/socket"), // allow to connect to /tmp/socket
)
then if the system only supports ABI v8, it will have to clear both bits so that connections to /tmp/socket work,
even though the scoped bit is technically supported on v8.
**This requires additional logic in client libraries**, similar to our "refer" semantics (which users often get wrong):
if (there is a rule that allows connections by path name)
clear_the_scoped_bit_as_well();
// even though the path name rule normally only affects a different bitIn contrast, if both the scoped bit and FS_RESOLVE_UNIX were added in the same ABI version, then if a user does the above call, we are either equal-or-above that ABI version, in which case it works, or we are below that ABI version, in which case the two bits already get cleared from the landlock_ruleset_attr through the existing backwards compatibility mechanism.
**In my mind, Justin is right that we should ideally introduce these together.** We have seen users implementing the "Refer" special case wrongly very often, it will likely happen here too, if we require extra logic in userspace libraries.
BTW, regarding the implementation: To have *OR* semantics for "within scope" and "allow-listed path", the implementation will be non-trivial, and I suspect we won't hit the merge window if we try to get them both in for 7.0. But in my mind, a simple UAPI is more important than trying to make it in time for the next merge window.
(The implementation is difficult because the path-based and scope-based check currently happen in different LSM hooks, and none of the two hooks has enough information to make the decision alone. The second hook only gets called if the first returns 0. It'll require some further discussion to make it work together.)
Quoted 46 lines
Furthermore, an application / Landlock config etc can always opt to not use the scope bit at all, if it "knows" all the locations where the application's sockets would be placed, and just use
RESOLVE_UNIXaccess right (or nothing if it is not supported).(The following is a bit of a side note, not terribly relevant if we're deciding to go with the patch as is.)
Quoted 24 lines
Quoted 14 lines
[...]
Another way to put it is that, if FS-based and scope-based controls interacts in the above proposed way, both mechanisms feel like "poking holes" in the other. But as Mickaël said, one can think of the two mechanisms not as independent controls, but rather as two interfaces for the same control. The socket access control is "enabled" if either theLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess is handled, or the scope bit proposed in this patch is enabled.With that said, I can think of some alternative ways that might make this API look "better" (from a subjective point of view, feedback welcome), however it does mean more delays, and specifically, these will depend on
LANDLOCK_ACCESS_FS_RESOLVE_UNIX:Quoted 9 lines
One possibility is to simply always allow a Landlock domain to connect to its own sockets (in the case where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled, otherwise all sockets are allowed). This might be reasonable, as one can only connect to a socket it creates if it has the permission to create it in the first place, which is already controlled byLANDLOCK_ACCESS_FS_MAKE_SOCK, so we don't really lose any policy flexibility here - if for some reason the sandboxer don't want to allow access to any (pathname) sockets, even the sandboxed app's own ones, it can just not allowLANDLOCK_ACCESS_FS_MAKE_SOCKanywhere.Quoted 3 lines
LANDLOCK_ACCESS_FS_MAKE_SOCKis only required to bind/listen to a socket, not to connect. I guess you was thinking aboutLANDLOCK_ACCESS_FS_RESOLVE_UNIXin this case?In this "allow same-scope connect unconditionally" proposal, the application would still be able to (bind to and) connect to its own sockets, even if
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled and nothing is allowed to haveLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess. But a sandboxer which for whatever reason doesn't want this "allow same scope" default can still prevent the use of (pathname) sockets by restrictingLANDLOCK_ACCESS_FS_MAKE_SOCK, because if an app can't connect to any sockets it doesn't own, and can't create any sockets itself either, then it effectively can't connect to any sockets at all.
Quoted 4 lines
(Although on second thought, I guess there could be a case where an app first creates some socket files before doing
landlock_restrict_self(), then it might still be able to bind to these even withoutLANDLOCK_ACCESS_FS_MAKE_SOCK?)
FWIW, I also really liked Tingmao's first of the two listed possibilities in [1], where she proposed to introduce both rights together. In my understanding, the arguments we have discussed so far for that are:
IN FAVOR:
(pro1) Connecting to a UNIX socket in the same scope is always safe,
and it makes it possible to use named UNIX sockets between the processes within a Landlock domains. (Mickaël convinced me in discussion at FOSDEM that this is true.)
If someone absolutely does not want that, they can restrict LANDLOCK_ACCESS_FS_MAKE_SOCK and achieve the same effect (as Tingmao said above).
(pro2) The implementation of this is simpler.
(I attempted to understand how the "or" semantics would be implemented, and I found it non-trivial when you try to do it for all layers at once. (Kernighan's Law applies, IMHO))
AGAINST:
(con1) It would work differently than the other scoped access rights
that we already have.
A speculative feature that could potentially be built with the scoped access rights is that we could add a rule to permit IPC to other Landlock scopes, e.g. introducing a new rule type
struct landlock_scope_attr {
__u64 allowed_access; /* for "scoped" bits */ /* some way to identify domains */
}
so that we could make IPC access to other Landlock domains configurable.
If the scoped bit and the FS RESOLVE_UNIX bit were both conflated in RESOLVE_UNIX, it would not be possible to make UNIX connections configurable in such a way.
(con2) Consistent behaviour between scoped flags and their
interactions with other access rights:
The existing scoped access rights (signal, abstract sockets) could hypothetically be extended with a related access right of another type. For instance, there could be an access right type
__u64 handled_signal_number;
and then you could add a rule to permit the use of certain signal numbers. The interaction between the scoped flags and other access rights should work the same.
Constructive Proposal for consideration: Why not both?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Why not do both what Tingmao proposed in [1] **and** reserve the option to add the matching "scoped flag" later?
* Introduce LANDLOCK_ACCESS_FS_RESOLVE_UNIX.
If it is handled, UNIX connections are allowed either:
(1) if the connection is to a service in the same scope, or
(2) if the path was allow-listed with a "path beneath" rule.
* Add LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET later, if needed.
Let's go through the arguments again:
We have observed that it is harmless to allow connections to services in the same scope (1), and that if users absolutely don't want that, they can actually prohibit it through LANDLOCK_ACCESS_FS_MAKE_SOCK (pro1).
(con1): Can we still implement the feature idea where we poke a hole
to get UNIX-connect() access to other Landlock domains?
I think the answer is yes. The implementation strategy is:
* Add the scoped bit LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
* The scoped bit can now be used to allow-list connections to other Landlock domains.
For users, just setting the scoped bit on its own does the same as handling LANDLOCK_ACCESS_FS_RESOLVE_UNIX. That way, the kernel-side implementation can also stay simple. The only reason why the scoped bit is needed is because it makes it possible to allow-list connections to other Landlock domains, but at the same time, it is safe if libraries set the scoped bit once it exists, as it does not have any bad runtime impact either.
(con2): Consistency: Do all the scoped flags interact with their
corresponding access rights in the same way?
The other scope flags do not have corresponding access rights, so far.
If we were to add corresponding access rights for the other scope flags, I would argue that we could apply a consistent logic there, because IPC access within the same scope is always safe:
- A hypothetical access right type for "signal numbers" would only restrict signals that go beyond the current scope.
- A hypothetical access right type for "abstract UNIX domain socket names" would only restrict connections to abstract UNIX domain servers that go beyond the current scope.
I can not come up with a scenario where this doesn't work.
In conclusion, I think the approach has significant upsides:
* Simpler UAPI: Users only have one access bit to deal with, in the near future. Once we do add a scope flag for UNIX connections, it does not interact in a surprising way with the corresponding FS access right, because with either of these, scoped access is allowed.
If users absolutely need to restrict scoped access, they can restrict LANDLOCK_ACCESS_FS_MAKE_SOCK. It is a slightly obscure API, but in line with the "make easy things easy, make hard things possible" API philosophy. And needing this should be the exception rather than the norm, after all.
* Consistent behaviour between scoped flags and regular access rights, also for speculative access rights affecting the existing scoped flags for signals and abstract UNIX domain sockets.
I know this was a slightly long mail, but I thought long and tried to be structured. Please let me know what you think.
—Günther
Quoted 54 lines · Günther Noack
Quoted 10 lines · Tingmao Wang
Quoted 8 lines · Justin Suess
Regardless if you merge the patch series now in 7.0 or a later version, I think there is something to be said about having the filesystem and scoped unix access right merged in the same ABI version / merge window.
As you pointed out earlier, the combination of the two flags is much flexible and useful to userspace consumers than one or the other, and if the features were merged separately, there would be an awkward middle ABI where user space consumers may have to make compromises or changes to sandbox between different versions or change application behavior.
[...]Quoted 12 lines
Given that the scope bit and
RESOLVE_UNIXaccess right are in some sense part of the same system (they interact in an OR manner, after all), there is some positive for having them introduced in the same version, but on the other hand, with my above reasoning, I don't think these two mechanisms (scope bit andRESOLVE_UNIXaccess) being in different ABI versions would be too much of a problem. In either case, for applications which require access to more "privileged" sockets, when running on a kernel without theRESOLVE_UNIXaccess right support, no pathname socket restrictions can be applied (i.e. it won't use the scope bit either, there isn't much "compromise" it can make here). On the other hand, ifRESOLVE_UNIXis supported, then it knows that the scope bit is also supported, and can just use it.Yes, but that does require additional subtle backwards compatibility logic in userspace libraries, to implement the "best effort" fallbacks.
Assuming the scoped bit is added in v8 and the
FS_RESOLVE_UNIXright in v9, if a user does this (in Go-landlock syntax):// restrict both scoped bit and FS
RESOLVE_UNIXright, if possible landlock.V9.BestEffort().RestrictPaths(
landlock.ResolveUnix("/tmp/socket"), // allow to connect to/tmp/socket
)then if the system only supports ABI v8, it will have to clear both bits so that connections to
/tmp/socketwork,
even though the scoped bit is technically supported on v8.**This requires additional logic in client libraries**, similar to our "refer" semantics (which users often get wrong):
if (there is a rule that allows connections by path name)
clear_the_scoped_bit_as_well(); // even though the path name rule normally only affects a different bitIn contrast, if both the scoped bit and
FS_RESOLVE_UNIXwere added in the same ABI version, then if a user does the above call, we are either equal-or-above that ABI version, in which case it works, or we are below that ABI version, in which case the two bits already get cleared from thelandlock_ruleset_attrthrough the existing backwards compatibility mechanism.
Quoted 4 lines
**In my mind, Justin is right that we should ideally introduce these together.** We have seen users implementing the "Refer" special case wrongly very often, it will likely happen here too, if we require extra logic in userspace libraries.
Ok, this makes sense to me. I will send the patch's next version as-is anyway for completeness since it's basically done but I recognize that we might change the plan based on this discussion.
Quoted 8 lines
BTW, regarding the implementation: To have *OR* semantics for "within scope" and "allow-listed path", the implementation will be non-trivial, and I suspect we won't hit the merge window if we try to get them both in for 7.0. But in my mind, a simple UAPI is more important than trying to make it in time for the next merge window.
Quoted 5 lines
(The implementation is difficult because the path-based and scope-based check currently happen in different LSM hooks, and none of the two hooks has enough information to make the decision alone. The second hook only gets called if the first returns 0. It'll require some further discussion to make it work together.)
Right. In that case, would it make sense to pass sk into the new security_unix_find() hook, perhaps with the new argument named struct sock *other? Then we can use this hook for the scope check as well by using landlock_cred(other->sk_socket->file->f_cred)->domain etc.
net/unix/af_unix.c +7 -7
@@ -1223,24 +1223,24 @@ static struct sock *unix_find_bsd(struct sockaddr_un *sunaddr, int addr_len, err = -ECONNREFUSED ; inode =d_backing_inode (path.dentry);if (!S_ISSOCK (inode->i_mode)) goto path_put; + err = -ECONNREFUSED ; + sk =unix_find_socket_byinode (inode); +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, type, flags); + err =security_unix_find (&path, sk, flags);if (err) - goto path_put; - - err = -ECONNREFUSED ; - sk =unix_find_socket_byinode (inode); -if (!sk) - goto path_put; + goto sock_put; err = -EPROTOTYPE ;if (sk->sk_type == type)touch_atime (&path);else goto sock_put;
By doing this we won't even need to pass type separately anymore. The only change would be that now one can determine if a socket is bound or not even without being allowed RESOLVE_UNIX access. I'm not sure how much of an issue this is, but we could also call the hook anyway with a NULL in place of the new argument, if unix_find_socket_byinode() fails. Other LSMs can then decide what to do in that case (either return -ECONNREFUSED or -EPERM).
Quoted 69 lines
Quoted 48 lines
Furthermore, an application / Landlock config etc can always opt to not use the scope bit at all, if it "knows" all the locations where the application's sockets would be placed, and just use
RESOLVE_UNIXaccess right (or nothing if it is not supported).(The following is a bit of a side note, not terribly relevant if we're deciding to go with the patch as is.)
Quoted 24 lines
Quoted 14 lines
[...]
Another way to put it is that, if FS-based and scope-based controls interacts in the above proposed way, both mechanisms feel like "poking holes" in the other. But as Mickaël said, one can think of the two mechanisms not as independent controls, but rather as two interfaces for the same control. The socket access control is "enabled" if either theLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess is handled, or the scope bit proposed in this patch is enabled.With that said, I can think of some alternative ways that might make this API look "better" (from a subjective point of view, feedback welcome), however it does mean more delays, and specifically, these will depend on
LANDLOCK_ACCESS_FS_RESOLVE_UNIX:Quoted 9 lines
One possibility is to simply always allow a Landlock domain to connect to its own sockets (in the case where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled, otherwise all sockets are allowed). This might be reasonable, as one can only connect to a socket it creates if it has the permission to create it in the first place, which is already controlled byLANDLOCK_ACCESS_FS_MAKE_SOCK, so we don't really lose any policy flexibility here - if for some reason the sandboxer don't want to allow access to any (pathname) sockets, even the sandboxed app's own ones, it can just not allowLANDLOCK_ACCESS_FS_MAKE_SOCKanywhere.Quoted 3 lines
LANDLOCK_ACCESS_FS_MAKE_SOCKis only required to bind/listen to a socket, not to connect. I guess you was thinking aboutLANDLOCK_ACCESS_FS_RESOLVE_UNIXin this case?In this "allow same-scope connect unconditionally" proposal, the application would still be able to (bind to and) connect to its own sockets, even if
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled and nothing is allowed to haveLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess. But a sandboxer which for whatever reason doesn't want this "allow same scope" default can still prevent the use of (pathname) sockets by restrictingLANDLOCK_ACCESS_FS_MAKE_SOCK, because if an app can't connect to any sockets it doesn't own, and can't create any sockets itself either, then it effectively can't connect to any sockets at all.Quoted 4 lines
(Although on second thought, I guess there could be a case where an app first creates some socket files before doing
landlock_restrict_self(), then it might still be able to bind to these even withoutLANDLOCK_ACCESS_FS_MAKE_SOCK?)FWIW, I also really liked Tingmao's first of the two listed possibilities in [1], where she proposed to introduce both rights together. In my understanding, the arguments we have discussed so far for that are:
IN FAVOR:
(pro1) Connecting to a UNIX socket in the same scope is always safe,
and it makes it possible to use named UNIX sockets between the processes within a Landlock domains. (Mickaël convinced me in discussion at FOSDEM that this is true.)If someone absolutely does not want that, they can restrict
LANDLOCK_ACCESS_FS_MAKE_SOCKand achieve the same effect (as Tingmao said above).
Quoted 5 lines
(pro2) The implementation of this is simpler.
(I attempted to understand how the "or" semantics would be implemented, and I found it non-trivial when you try to do it for all layers at once. (Kernighan's Law applies, IMHO))
I think the logic would basically be:
1. if any layers deny the access due to handled RESOLVE_UNIX but does not have the scope bit set, then we will deny rightaway, without calling domain_is_scoped().
2. Call domain_is_scoped() with a bitmask of "rules_covered" layers where there are RESOLVE_UNIX rules covering the socket being accessed, and essentially ignore those layers in the scope violation check.
I definitely agree that it is tricky, but making same-scope access be allowed (i.e. the suggested idea above) would only get rid of step 1, which I think is the "simpler" bit. The extra logic in step 2 is still needed.
I definitely agree with pro1 tho.
Quoted 15 lines
AGAINST:
(con1) It would work differently than the other scoped access rights
that we already have.A speculative feature that could potentially be built with the scoped access rights is that we could add a rule to permit IPC to other Landlock scopes, e.g. introducing a new rule type
struct
landlock_scope_attr{
__u64allowed_access; /* for "scoped" bits */ /* some way to identify domains */
}
Quoted 6 lines
so that we could make IPC access to other Landlock domains configurable.
If the scoped bit and the FS
RESOLVE_UNIXbit were both conflated inRESOLVE_UNIX, it would not be possible to make UNIX connections configurable in such a way.
This exact API would no longer work, but if we give up the equivalence between scope bits and the landlock_scope_attr struct, then we can do something like:
struct landlock_scope_attr {
__u64 ptrace:1; /* Note that this is not a (user controllable) scope bit! */
__u64 abstract_unix_socket:1;
__u64 pathname_unix_socket:1;
/* ... */
__u64 allowed_signals;
/*
* some way to identify domains, maybe we could use the audit domain
* ID, with 0 denoting "allow access to non-Landlocked processes?
*/
}Quoted 10 lines
(con2) Consistent behaviour between scoped flags and their
interactions with other access rights:The existing scoped access rights (signal, abstract sockets) could hypothetically be extended with a related access right of another type. For instance, there could be an access right type
__u64handled_signal_number;
Quoted 7 lines
and then you could add a rule to permit the use of certain signal numbers. The interaction between the scoped flags and other access rights should work the same.
Constructive Proposal for consideration: Why not both?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
I will think about the following a bit more but I'm afraid that I feel like it might get slightly confusing. With this, the only reason for having LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET is for API consistency when we later enable allowing access to other domains (if I understood correctly), in which case I personally feel like the suggestion on landlock_scope_attr above, where we essentially accept that it is decoupled with the scope bits in the ruleset, might be simpler...?
Quoted 83 lines
Why not do both what Tingmao proposed in [1] **and** reserve the option to add the matching "scoped flag" later?
* Introduce
LANDLOCK_ACCESS_FS_RESOLVE_UNIX.If it is handled, UNIX connections are allowed either:
(1) if the connection is to a service in the same scope, or
(2) if the path was allow-listed with a "path beneath" rule.* Add
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETlater, if needed.Let's go through the arguments again:
We have observed that it is harmless to allow connections to services in the same scope (1), and that if users absolutely don't want that, they can actually prohibit it through
LANDLOCK_ACCESS_FS_MAKE_SOCK(pro1).(con1): Can we still implement the feature idea where we poke a hole
to get UNIX-connect() access to other Landlock domains?I think the answer is yes. The implementation strategy is:
* Add the scoped bit
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
* The scoped bit can now be used to allow-list connections to other Landlock domains.For users, just setting the scoped bit on its own does the same as handling
LANDLOCK_ACCESS_FS_RESOLVE_UNIX. That way, the kernel-side implementation can also stay simple. The only reason why the scoped bit is needed is because it makes it possible to allow-list connections to other Landlock domains, but at the same time, it is safe if libraries set the scoped bit once it exists, as it does not have any bad runtime impact either.(con2): Consistency: Do all the scoped flags interact with their
corresponding access rights in the same way?The other scope flags do not have corresponding access rights, so far.
If we were to add corresponding access rights for the other scope flags, I would argue that we could apply a consistent logic there, because IPC access within the same scope is always safe:
- A hypothetical access right type for "signal numbers" would only restrict signals that go beyond the current scope.
- A hypothetical access right type for "abstract UNIX domain socket names" would only restrict connections to abstract UNIX domain servers that go beyond the current scope.
I can not come up with a scenario where this doesn't work.
In conclusion, I think the approach has significant upsides:
* Simpler UAPI: Users only have one access bit to deal with, in the near future. Once we do add a scope flag for UNIX connections, it does not interact in a surprising way with the corresponding FS access right, because with either of these, scoped access is allowed.
If users absolutely need to restrict scoped access, they can restrict
LANDLOCK_ACCESS_FS_MAKE_SOCK. It is a slightly obscure API, but in line with the "make easy things easy, make hard things possible" API philosophy. And needing this should be the exception rather than the norm, after all.* Consistent behaviour between scoped flags and regular access rights, also for speculative access rights affecting the existing scoped flags for signals and abstract UNIX domain sockets.
I know this was a slightly long mail, but I thought long and tried to be structured. Please let me know what you think.
—Günther
Günther Noack
reviewerQuoted 55 lines · Tingmao Wang
Quoted 11 lines · Günther Noack
BTW, regarding the implementation: To have *OR* semantics for "within scope" and "allow-listed path", the implementation will be non-trivial, and I suspect we won't hit the merge window if we try to get them both in for 7.0. But in my mind, a simple UAPI is more important than trying to make it in time for the next merge window.
(The implementation is difficult because the path-based and scope-based check currently happen in different LSM hooks, and none of the two hooks has enough information to make the decision alone. The second hook only gets called if the first returns 0. It'll require some further discussion to make it work together.)
Right. In that case, would it make sense to pass sk into the new
security_unix_find()hook, perhaps with the new argument namedstruct sock *other? Then we can use this hook for the scope check as well by using landlock_cred(other->sk_socket->file->f_cred)->domain etc.net/unix/af_unix.c +7
-7@@ -1223,24 +1223,24 @@ static struct sock *unix_find_bsd(struct sockaddr_un *sunaddr, int addr_len, err = -ECONNREFUSED ; inode =d_backing_inode (path.dentry);if (!S_ISSOCK (inode->i_mode)) goto path_put; + err = -ECONNREFUSED ; + sk =unix_find_socket_byinode (inode); +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, type, flags); + err =security_unix_find (&path, sk, flags);if (err) - goto path_put; - - err = -ECONNREFUSED ; - sk =unix_find_socket_byinode (inode); -if (!sk) - goto path_put; + goto sock_put; err = -EPROTOTYPE ;if (sk->sk_type == type)touch_atime (&path);else goto sock_put;
Quoted 7 lines
By doing this we won't even need to pass
typeseparately anymore. The only change would be that now one can determine if a socket is bound or not even without being allowedRESOLVE_UNIXaccess. I'm not sure how much of an issue this is, but we could also call the hook anyway with a NULL in place of the new argument, ifunix_find_socket_byinode()fails. Other LSMs can then decide what to do in that case (either return-ECONNREFUSEDor-EPERM).
Thank you for the suggestion.
Small caveat is that the LSM interface is very central and we should be careful. We have previously gotten the advice from Paul to design the hooks in an LSM-independent way that ideally reflects the arguments to the unix_find_bsd() function, and this would now deviate (slightly) from that, but simplifying the implementation for us. In my personal opinion, this might be worth doing the trade-off, if AppArmor people also agree, but we should double check.
To keep the discussion of implementation and interface separate, I have raised this question in the pathname-restricted-UNIX patch set thread in [1].
[1] https://lore.kernel.org/all/[email protected]/
Quoted 83 lines
Quoted 67 lines
Quoted 46 lines
Furthermore, an application / Landlock config etc can always opt to not use the scope bit at all, if it "knows" all the locations where the application's sockets would be placed, and just use
RESOLVE_UNIXaccess right (or nothing if it is not supported).(The following is a bit of a side note, not terribly relevant if we're deciding to go with the patch as is.)
Quoted 24 lines
Quoted 14 lines
[...]
Another way to put it is that, if FS-based and scope-based controls interacts in the above proposed way, both mechanisms feel like "poking holes" in the other. But as Mickaël said, one can think of the two mechanisms not as independent controls, but rather as two interfaces for the same control. The socket access control is "enabled" if either theLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess is handled, or the scope bit proposed in this patch is enabled.With that said, I can think of some alternative ways that might make this API look "better" (from a subjective point of view, feedback welcome), however it does mean more delays, and specifically, these will depend on
LANDLOCK_ACCESS_FS_RESOLVE_UNIX:Quoted 9 lines
One possibility is to simply always allow a Landlock domain to connect to its own sockets (in the case where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled, otherwise all sockets are allowed). This might be reasonable, as one can only connect to a socket it creates if it has the permission to create it in the first place, which is already controlled byLANDLOCK_ACCESS_FS_MAKE_SOCK, so we don't really lose any policy flexibility here - if for some reason the sandboxer don't want to allow access to any (pathname) sockets, even the sandboxed app's own ones, it can just not allowLANDLOCK_ACCESS_FS_MAKE_SOCKanywhere.Quoted 3 lines
LANDLOCK_ACCESS_FS_MAKE_SOCKis only required to bind/listen to a socket, not to connect. I guess you was thinking aboutLANDLOCK_ACCESS_FS_RESOLVE_UNIXin this case?In this "allow same-scope connect unconditionally" proposal, the application would still be able to (bind to and) connect to its own sockets, even if
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled and nothing is allowed to haveLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess. But a sandboxer which for whatever reason doesn't want this "allow same scope" default can still prevent the use of (pathname) sockets by restrictingLANDLOCK_ACCESS_FS_MAKE_SOCK, because if an app can't connect to any sockets it doesn't own, and can't create any sockets itself either, then it effectively can't connect to any sockets at all.Quoted 4 lines
(Although on second thought, I guess there could be a case where an app first creates some socket files before doing
landlock_restrict_self(), then it might still be able to bind to these even withoutLANDLOCK_ACCESS_FS_MAKE_SOCK?)FWIW, I also really liked Tingmao's first of the two listed possibilities in [1], where she proposed to introduce both rights together. In my understanding, the arguments we have discussed so far for that are:
IN FAVOR:
(pro1) Connecting to a UNIX socket in the same scope is always safe,
and it makes it possible to use named UNIX sockets between the processes within a Landlock domains. (Mickaël convinced me in discussion at FOSDEM that this is true.)If someone absolutely does not want that, they can restrict
LANDLOCK_ACCESS_FS_MAKE_SOCKand achieve the same effect (as Tingmao said above).Quoted 5 lines
(pro2) The implementation of this is simpler.
(I attempted to understand how the "or" semantics would be implemented, and I found it non-trivial when you try to do it for all layers at once. (Kernighan's Law applies, IMHO))
I think the logic would basically be:
1. if any layers deny the access due to handled
RESOLVE_UNIXbut does not have the scope bit set, then we will deny rightaway, without callingdomain_is_scoped().2. Call
domain_is_scoped()with a bitmask of "rules_covered" layers where there areRESOLVE_UNIXrules covering the socket being accessed, and essentially ignore those layers in the scope violation check.
Quoted 6 lines
I definitely agree that it is tricky, but making same-scope access be allowed (i.e. the suggested idea above) would only get rid of step 1, which I think is the "simpler" bit. The extra logic in step 2 is still needed.
I definitely agree with pro1 tho.
Yes, you are describing the logic for the variant where LANDLOCK_ACCESS_FS_RESOLVE_UNIX does not implicitly permit access from within the same scope. In that variant, there can be situations where the first hook can deny the action immediately.
In the variant where LANDLOCK_ACCESS_FS_RESOLVE_UNIX *does* implicitly allow access from within the same scope, that shortcutting is not possible. On the upside however, there is no need to distinguish whether the scope flag is set when we are in the security_unix_find() hook, because access from within the same scope is always permitted. (That is the simplification I meant.)
Quoted 25 lines
Quoted 14 lines
AGAINST:
(con1) It would work differently than the other scoped access rights
that we already have.A speculative feature that could potentially be built with the scoped access rights is that we could add a rule to permit IPC to other Landlock scopes, e.g. introducing a new rule type
struct
landlock_scope_attr{
__u64allowed_access; /* for "scoped" bits */ /* some way to identify domains */
}Quoted 6 lines
so that we could make IPC access to other Landlock domains configurable.
If the scoped bit and the FS
RESOLVE_UNIXbit were both conflated inRESOLVE_UNIX, it would not be possible to make UNIX connections configurable in such a way.This exact API would no longer work, but if we give up the equivalence between scope bits and the
landlock_scope_attrstruct, then we can do something like:
Quoted 13 lines
struct landlock_scope_attr {
__u64 ptrace:1; /* Note that this is not a (user controllable) scope bit! */
__u64 abstract_unix_socket:1;
__u64 pathname_unix_socket:1;
/* ... */
__u64 allowed_signals;
/*
* some way to identify domains, maybe we could use the audit domain
* ID, with 0 denoting "allow access to non-Landlocked processes?
*/
}Yes, it would be possible to use such a struct for that scenario where IPC access gets allowed for other Landlock scopes. It would mean that we would not need to introduce a scoped flag for the pathname UNIX socket connections. But the relationship between that struct landlock_scope_attr and the flags and access rights in struct landlock_ruleset_attr would become less clear, which is a slight downside, and maybe error prone for users to work with.
If we introduced an additional scoped flag, it would also be consistent though.
(con1) was written under the assumption that we do not have an additional scoped flag. If that is lacking, it is not possible to express UNIX connect() access to other Landlock domains with that struct. But as outlined in the proposal below, if we *do* (later) introduce the additional scoped flag *in addition* to the FS access right, this *both* stays consistent in semantics with the signal and abstract UNIX support, *and* it starts working in a world where ICP access can be allowed to talk to other Landlock domains.
Quoted 17 lines
Quoted 9 lines
(con2) Consistent behaviour between scoped flags and their
interactions with other access rights:The existing scoped access rights (signal, abstract sockets) could hypothetically be extended with a related access right of another type. For instance, there could be an access right type
__u64handled_signal_number;Quoted 7 lines
and then you could add a rule to permit the use of certain signal numbers. The interaction between the scoped flags and other access rights should work the same.
Constructive Proposal for consideration: Why not both?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Quoted 7 lines
I will think about the following a bit more but I'm afraid that I feel like it might get slightly confusing. With this, the only reason for having
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETis for API consistency when we later enable allowing access to other domains (if I understood correctly), in which case I personally feel like the suggestion onlandlock_scope_attrabove, where we essentially accept that it is decoupled with the scope bits in the ruleset, might be simpler...?
Mickaël expressed the opinion to me that he would like to APIs to stay consistent between signals, abstract UNIX sockets, named UNIX sockets and other future "scoped" operations, in scenarios where:
* the "scoped" (IPC) operations can be configured to give access to other Landlock domains (and that should work for UNIX connections too)
* the existing "scoped" operations also start having matching access rights
I think with the way I proposed, that would be consistent.
Quoted 71 lines
Quoted 71 lines
Why not do both what Tingmao proposed in [1] **and** reserve the option to add the matching "scoped flag" later?
* Introduce
LANDLOCK_ACCESS_FS_RESOLVE_UNIX.If it is handled, UNIX connections are allowed either:
(1) if the connection is to a service in the same scope, or
(2) if the path was allow-listed with a "path beneath" rule.* Add
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETlater, if needed.Let's go through the arguments again:
We have observed that it is harmless to allow connections to services in the same scope (1), and that if users absolutely don't want that, they can actually prohibit it through
LANDLOCK_ACCESS_FS_MAKE_SOCK(pro1).(con1): Can we still implement the feature idea where we poke a hole
to get UNIX-connect() access to other Landlock domains?I think the answer is yes. The implementation strategy is:
* Add the scoped bit
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
* The scoped bit can now be used to allow-list connections to other Landlock domains.For users, just setting the scoped bit on its own does the same as handling
LANDLOCK_ACCESS_FS_RESOLVE_UNIX. That way, the kernel-side implementation can also stay simple. The only reason why the scoped bit is needed is because it makes it possible to allow-list connections to other Landlock domains, but at the same time, it is safe if libraries set the scoped bit once it exists, as it does not have any bad runtime impact either.(con2): Consistency: Do all the scoped flags interact with their
corresponding access rights in the same way?The other scope flags do not have corresponding access rights, so far.
If we were to add corresponding access rights for the other scope flags, I would argue that we could apply a consistent logic there, because IPC access within the same scope is always safe:
- A hypothetical access right type for "signal numbers" would only restrict signals that go beyond the current scope.
- A hypothetical access right type for "abstract UNIX domain socket names" would only restrict connections to abstract UNIX domain servers that go beyond the current scope.
I can not come up with a scenario where this doesn't work.
In conclusion, I think the approach has significant upsides:
* Simpler UAPI: Users only have one access bit to deal with, in the near future. Once we do add a scope flag for UNIX connections, it does not interact in a surprising way with the corresponding FS access right, because with either of these, scoped access is allowed.
If users absolutely need to restrict scoped access, they can restrict
LANDLOCK_ACCESS_FS_MAKE_SOCK. It is a slightly obscure API, but in line with the "make easy things easy, make hard things possible" API philosophy. And needing this should be the exception rather than the norm, after all.
Quoted 5 lines
Quoted 5 lines
* Consistent behaviour between scoped flags and regular access rights, also for speculative access rights affecting the existing scoped flags for signals and abstract UNIX domain sockets.
—Günther
Quoted 13 lines · Günther Noack
Quoted 12 lines · Tingmao Wang
Quoted 11 lines · Günther Noack
BTW, regarding the implementation: To have *OR* semantics for "within scope" and "allow-listed path", the implementation will be non-trivial, and I suspect we won't hit the merge window if we try to get them both in for 7.0. But in my mind, a simple UAPI is more important than trying to make it in time for the next merge window.
(The implementation is difficult because the path-based and scope-based check currently happen in different LSM hooks, and none of the two hooks has enough information to make the decision alone. The second hook only gets called if the first returns 0. It'll require some further discussion to make it work together.)
Quoted 4 lines
Quoted 4 lines
Right. In that case, would it make sense to pass sk into the new
security_unix_find()hook, perhaps with the new argument namedstruct sock *other? Then we can use this hook for the scope check as well by using landlock_cred(other->sk_socket->file->f_cred)->domain etc.
Tingmao,
This seems like the best way to do it. Alternatively, I considered passing in just the f_cred to the hook, but I'm leaning towards a more generic implementation like the one you have below.
Quoted 38 lines
Quoted 38 lines
net/unix/af_unix.c +7
-7@@ -1223,24 +1223,24 @@ static struct sock *unix_find_bsd(struct sockaddr_un *sunaddr, int addr_len, err = -ECONNREFUSED ; inode =d_backing_inode (path.dentry);if (!S_ISSOCK (inode->i_mode)) goto path_put; + err = -ECONNREFUSED ; + sk =unix_find_socket_byinode (inode); +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, type, flags); + err =security_unix_find (&path, sk, flags);if (err) - goto path_put; - - err = -ECONNREFUSED ; - sk =unix_find_socket_byinode (inode); -if (!sk) - goto path_put; + goto sock_put; err = -EPROTOTYPE ;if (sk->sk_type == type)touch_atime (&path);else goto sock_put;
Quoted 7 lines
Quoted 7 lines
By doing this we won't even need to pass
typeseparately anymore. The only change would be that now one can determine if a socket is bound or not even without being allowedRESOLVE_UNIXaccess. I'm not sure how much of an issue this is, but we could also call the hook anyway with a NULL in place of the new argument, ifunix_find_socket_byinode()fails. Other LSMs can then decide what to do in that case (either return-ECONNREFUSEDor-EPERM).
I think landlock already allows checking of existence of files even when when the process doesn't have rights on them, so there is precedent for this so in my mind this would probably be OK.
Quoted 9 lines
Thank you for the suggestion.
Small caveat is that the LSM interface is very central and we should be careful. We have previously gotten the advice from Paul to design the hooks in an LSM-independent way that ideally reflects the arguments to the
unix_find_bsd()function, and this would now deviate (slightly) from that, but simplifying the implementation for us. In my personal opinion, this might be worth doing the trade-off, if AppArmor people also agree, but we should double check.
Gunther:
I'd be happy to send you an updated LSM hook patch with the sock parameter.
Just let me know.
Quoted 87 lines
To keep the discussion of implementation and interface separate, I have raised this question in the pathname-restricted-UNIX patch set thread in [1].
[1] https://lore.kernel.org/all/[email protected]/
Quoted 78 lines
Quoted 64 lines
Quoted 44 lines
Furthermore, an application / Landlock config etc can always opt to not use the scope bit at all, if it "knows" all the locations where the application's sockets would be placed, and just use
RESOLVE_UNIXaccess right (or nothing if it is not supported).(The following is a bit of a side note, not terribly relevant if we're deciding to go with the patch as is.)
Quoted 23 lines
Quoted 14 lines
[...]
Another way to put it is that, if FS-based and scope-based controls interacts in the above proposed way, both mechanisms feel like "poking holes" in the other. But as Mickaël said, one can think of the two mechanisms not as independent controls, but rather as two interfaces for the same control. The socket access control is "enabled" if either theLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess is handled, or the scope bit proposed in this patch is enabled.With that said, I can think of some alternative ways that might make this API look "better" (from a subjective point of view, feedback welcome), however it does mean more delays, and specifically, these will depend on
LANDLOCK_ACCESS_FS_RESOLVE_UNIX:Quoted 9 lines
One possibility is to simply always allow a Landlock domain to connect to its own sockets (in the case where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled, otherwise all sockets are allowed). This might be reasonable, as one can only connect to a socket it creates if it has the permission to create it in the first place, which is already controlled byLANDLOCK_ACCESS_FS_MAKE_SOCK, so we don't really lose any policy flexibility here - if for some reason the sandboxer don't want to allow access to any (pathname) sockets, even the sandboxed app's own ones, it can just not allowLANDLOCK_ACCESS_FS_MAKE_SOCKanywhere.Quoted 3 lines
LANDLOCK_ACCESS_FS_MAKE_SOCKis only required to bind/listen to a socket, not to connect. I guess you was thinking aboutLANDLOCK_ACCESS_FS_RESOLVE_UNIXin this case?In this "allow same-scope connect unconditionally" proposal, the application would still be able to (bind to and) connect to its own sockets, even if
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled and nothing is allowed to haveLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess. But a sandboxer which for whatever reason doesn't want this "allow same scope" default can still prevent the use of (pathname) sockets by restrictingLANDLOCK_ACCESS_FS_MAKE_SOCK, because if an app can't connect to any sockets it doesn't own, and can't create any sockets itself either, then it effectively can't connect to any sockets at all.Quoted 4 lines
(Although on second thought, I guess there could be a case where an app first creates some socket files before doing
landlock_restrict_self(), then it might still be able to bind to these even withoutLANDLOCK_ACCESS_FS_MAKE_SOCK?)FWIW, I also really liked Tingmao's first of the two listed possibilities in [1], where she proposed to introduce both rights together. In my understanding, the arguments we have discussed so far for that are:
IN FAVOR:
(pro1) Connecting to a UNIX socket in the same scope is always safe,
and it makes it possible to use named UNIX sockets between the processes within a Landlock domains. (Mickaël convinced me in discussion at FOSDEM that this is true.)If someone absolutely does not want that, they can restrict
LANDLOCK_ACCESS_FS_MAKE_SOCKand achieve the same effect (as Tingmao said above).Quoted 5 lines
(pro2) The implementation of this is simpler.
(I attempted to understand how the "or" semantics would be implemented, and I found it non-trivial when you try to do it for all layers at once. (Kernighan's Law applies, IMHO))
I think the logic would basically be:
1. if any layers deny the access due to handled
RESOLVE_UNIXbut does not have the scope bit set, then we will deny rightaway, without callingdomain_is_scoped().2. Call
domain_is_scoped()with a bitmask of "rules_covered" layers where there areRESOLVE_UNIXrules covering the socket being accessed, and essentially ignore those layers in the scope violation check.
Tingmao:
For connecting a pathname unix socket, the order of the hooks landlock sees is something like:
1. security_unix_find. (to look up the paths)
2. security_unix_may_send, security_unix_stream_connect (after the path is looked up)
Which for is called in DGRAM:
unix_dgram_connect OR unix_dgram_sendmsg
and for STREAM:
unix_stream_connect
IIRC, the path lookup only occurs in this order always, so in the logic as you have it the domain_is_scoped() would be called twice, once from the security_unix_find when you call it in step two, and once from the domain scope hooks. (If access was allowed from security_unix_find)
There are a couple of things to consider.
---
Audit blockers need special handling:
Here's an example:
1. Program A enforces a ruleset with RESOLVE_UNIX and the unix pathname scope bit, with no rules with that access bit (deny all for RESOLVE_UNIX).
2. Program A connects to /tmp/mysock.sock ran by program B, which is outside the domain.
2. security_unix_find is hit to lookup the path, and the RESOLVE_UNIX code doesn't grant access to /tmp/mysock.sock, so it calls domain_is_scoped
3. domain_is_scoped denies it as well, so now we must log an audit record.
When logging the denial, we have to include both blockers "scope.unix_socket" and "fs.resolve_unix" for the
denial, because it is the absence of both that caused the denial. I think the refer right has similar cases for auditing, so there is precedent for this (multiple blockers for an audit message).
---
Dual lookup for domain_is_scoped. Consider this case:
1. Program A enforces a ruleset with RESOLVE_UNIX and the unix pathname scope bit, with no rules with that access bit (deny all for RESOLVE_UNIX).
2. Program A connects to Program C's /tmp/foo.sock, which for the purposes of this example is in the domain of program A.
3. security_unix_find is hit to lookup the path, and the RESOLVE_UNIX code doesn't grant access to /tmp/mysock.sock, so it calls domain_is_scoped. Access is granted, and continues. (LSM hook complete)
4. The connection proceeds past the path lookup stage, and now security_unix_may_send, or security_unix_stream_connect gets called. This requires ANOTHER domain_is_scoped access check.
While I don't THINK this introduces a TOCTOU, it is a little confusing.
This does mean that we look up the domain twice, if this is implemented naively. I think we can then just skip the task credential checks then for security_unix_may_send and security_unix_stream_connect **for connecting to pathname sockets**, since the domain_is_scoped will already have been called in landlock's security_unix_find hook, eliminating the need for handling pathname socket domain checks layer on.
Quoted 104 lines
Quoted 7 lines
I definitely agree that it is tricky, but making same-scope access be allowed (i.e. the suggested idea above) would only get rid of step 1, which I think is the "simpler" bit. The extra logic in step 2 is still needed.
I definitely agree with pro1 tho.
Yes, you are describing the logic for the variant where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXdoes not implicitly permit access from within the same scope. In that variant, there can be situations where the first hook can deny the action immediately.In the variant where
LANDLOCK_ACCESS_FS_RESOLVE_UNIX*does* implicitly allow access from within the same scope, that shortcutting is not possible. On the upside however, there is no need to distinguish whether the scope flag is set when we are in thesecurity_unix_find()hook, because access from within the same scope is always permitted. (That is the simplification I meant.)Quoted 24 lines
Quoted 14 lines
AGAINST:
(con1) It would work differently than the other scoped access rights
that we already have.A speculative feature that could potentially be built with the scoped access rights is that we could add a rule to permit IPC to other Landlock scopes, e.g. introducing a new rule type
struct
landlock_scope_attr{
__u64allowed_access; /* for "scoped" bits */ /* some way to identify domains */
}Quoted 6 lines
so that we could make IPC access to other Landlock domains configurable.
If the scoped bit and the FS
RESOLVE_UNIXbit were both conflated inRESOLVE_UNIX, it would not be possible to make UNIX connections configurable in such a way.This exact API would no longer work, but if we give up the equivalence between scope bits and the
landlock_scope_attrstruct, then we can do something like:Quoted 13 lines
struct landlock_scope_attr { __u64 ptrace:1; /* Note that this is not a (user controllable) scope bit! */ __u64 abstract_unix_socket:1; __u64 pathname_unix_socket:1; /* ... */ __u64 allowed_signals; /* * some way to identify domains, maybe we could use the audit domain * ID, with 0 denoting "allow access to non-Landlocked processes? */ }Yes, it would be possible to use such a struct for that scenario where IPC access gets allowed for other Landlock scopes. It would mean that we would not need to introduce a scoped flag for the pathname UNIX socket connections. But the relationship between that struct
landlock_scope_attrand the flags and access rights in structlandlock_ruleset_attrwould become less clear, which is a slight downside, and maybe error prone for users to work with.If we introduced an additional scoped flag, it would also be consistent though.
(con1) was written under the assumption that we do not have an additional scoped flag. If that is lacking, it is not possible to express UNIX
connect()access to other Landlock domains with that struct. But as outlined in the proposal below, if we *do* (later) introduce the additional scoped flag *in addition* to the FS access right, this *both* stays consistent in semantics with the signal and abstract UNIX support, *and* it starts working in a world where ICP access can be allowed to talk to other Landlock domains.Quoted 16 lines
Quoted 9 lines
(con2) Consistent behaviour between scoped flags and their
interactions with other access rights:The existing scoped access rights (signal, abstract sockets) could hypothetically be extended with a related access right of another type. For instance, there could be an access right type
__u64handled_signal_number;Quoted 7 lines
and then you could add a rule to permit the use of certain signal numbers. The interaction between the scoped flags and other access rights should work the same.
Constructive Proposal for consideration: Why not both?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Quoted 7 lines
I will think about the following a bit more but I'm afraid that I feel like it might get slightly confusing. With this, the only reason for having
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETis for API consistency when we later enable allowing access to other domains (if I understood correctly), in which case I personally feel like the suggestion onlandlock_scope_attrabove, where we essentially accept that it is decoupled with the scope bits in the ruleset, might be simpler...?Mickaël expressed the opinion to me that he would like to APIs to stay consistent between signals, abstract UNIX sockets, named UNIX sockets and other future "scoped" operations, in scenarios where:
Quoted 62 lines
* the "scoped" (IPC) operations can be configured to give access to other Landlock domains (and that should work for UNIX connections too)
* the existing "scoped" operations also start having matching access rightsI think with the way I proposed, that would be consistent.
Quoted 55 lines
Quoted 55 lines
Why not do both what Tingmao proposed in [1] **and** reserve the option to add the matching "scoped flag" later?
* Introduce
LANDLOCK_ACCESS_FS_RESOLVE_UNIX.If it is handled, UNIX connections are allowed either:
(1) if the connection is to a service in the same scope, or
(2) if the path was allow-listed with a "path beneath" rule.* Add
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETlater, if needed.Let's go through the arguments again:
We have observed that it is harmless to allow connections to services in the same scope (1), and that if users absolutely don't want that, they can actually prohibit it through
LANDLOCK_ACCESS_FS_MAKE_SOCK(pro1).(con1): Can we still implement the feature idea where we poke a hole
to get UNIX-connect() access to other Landlock domains?I think the answer is yes. The implementation strategy is:
* Add the scoped bit
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
* The scoped bit can now be used to allow-list connections to other Landlock domains.For users, just setting the scoped bit on its own does the same as handling
LANDLOCK_ACCESS_FS_RESOLVE_UNIX. That way, the kernel-side implementation can also stay simple. The only reason why the scoped bit is needed is because it makes it possible to allow-list connections to other Landlock domains, but at the same time, it is safe if libraries set the scoped bit once it exists, as it does not have any bad runtime impact either.(con2): Consistency: Do all the scoped flags interact with their
corresponding access rights in the same way?The other scope flags do not have corresponding access rights, so far.
If we were to add corresponding access rights for the other scope flags, I would argue that we could apply a consistent logic there, because IPC access within the same scope is always safe:
- A hypothetical access right type for "signal numbers" would only restrict signals that go beyond the current scope.
- A hypothetical access right type for "abstract UNIX domain socket names" would only restrict connections to abstract UNIX domain servers that go beyond the current scope.
I can not come up with a scenario where this doesn't work.
Gunther / Tingmao / Mickaël:
I have a potential idea to make this concept cleaner.
The docs for landlock currently say:
IPC scoping does not support exceptions via landlock_add_rule(2). If an operation is scoped within a domain, no rules can be added to allow access to resources or processes outside of the scope.
So if we go with the solution where we are now saying IPC scoping DOES support exceptions we will need to update the documentation, to say scoping for pathname unix sockets is an exception, and have to have the "exemptible scopes" (like this one) alongside "non-exemptible" scopes (ie the existing ones). This creates some friction for users.
If we foresee other "exempt-able scopes" (which are scopes that also support creating exemptions w/ corresponding access rights) in the future, maybe we should consider separating the two in the ruleset attributes (I used scoped_fs as an example for the attribute name):
structlandlock_ruleset_attrruleset_attr={
.handled_access_fs=
LANDLOCK_ACCESS_FS_EXECUTE|
LANDLOCK_ACCESS_FS_WRITE_FILE|
LANDLOCK_ACCESS_FS_READ_FILE|
LANDLOCK_ACCESS_FS_READ_DIR|
LANDLOCK_ACCESS_FS_REMOVE_DIR|
LANDLOCK_ACCESS_FS_REMOVE_FILE|
LANDLOCK_ACCESS_FS_MAKE_CHAR|
LANDLOCK_ACCESS_FS_MAKE_DIR|
LANDLOCK_ACCESS_FS_MAKE_REG|
LANDLOCK_ACCESS_FS_MAKE_SOCK|
LANDLOCK_ACCESS_FS_MAKE_FIFO|
LANDLOCK_ACCESS_FS_MAKE_BLOCK|
LANDLOCK_ACCESS_FS_MAKE_SYM|
LANDLOCK_ACCESS_FS_REFER|
LANDLOCK_ACCESS_FS_TRUNCATE|LANDLOCK_ACCESS_FS_IOCTL_DEV,
.handled_access_net=
LANDLOCK_ACCESS_NET_BIND_TCP|LANDLOCK_ACCESS_NET_CONNECT_TCP,
.scoped=
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET|LANDLOCK_SCOPE_SIGNAL,
.scoped_fs =
LANDLOCK_SCOPE_FS_PATHNAME_UNIX_SOCKET
};
This more clearly distinguishes between scopes that have exceptions/corresponding fs rights, and ones that don't. Later we could add scoped_net, if needed. I feel like this would be more intuitive and better categorize future scoping rights. An obvious con is increasing the size of the ruleset attributes.
Of course this separation is only worth it if there are other "exempt-able" rights in the future. I can think of a few potential future rights which COULD be scoped and have corresponding rights (binder, sysv-ipc, pipes, tcp/udp between two local programs).
Quoted 22 lines
Quoted 16 lines
Quoted 16 lines
In conclusion, I think the approach has significant upsides:
* Simpler UAPI: Users only have one access bit to deal with, in the near future. Once we do add a scope flag for UNIX connections, it does not interact in a surprising way with the corresponding FS access right, because with either of these, scoped access is allowed.
If users absolutely need to restrict scoped access, they can restrict
LANDLOCK_ACCESS_FS_MAKE_SOCK. It is a slightly obscure API, but in line with the "make easy things easy, make hard things possible" API philosophy. And needing this should be the exception rather than the norm, after all.Quoted 5 lines
Quoted 5 lines
* Consistent behaviour between scoped flags and regular access rights, also for speculative access rights affecting the existing scoped flags for signals and abstract UNIX domain sockets.
—Günther
Mickaël Salaün
maintainerQuoted 64 lines · Justin Suess
Quoted 13 lines · Günther Noack
Quoted 12 lines · Tingmao Wang
Quoted 11 lines · Günther Noack
BTW, regarding the implementation: To have *OR* semantics for "within scope" and "allow-listed path", the implementation will be non-trivial, and I suspect we won't hit the merge window if we try to get them both in for 7.0. But in my mind, a simple UAPI is more important than trying to make it in time for the next merge window.
(The implementation is difficult because the path-based and scope-based check currently happen in different LSM hooks, and none of the two hooks has enough information to make the decision alone. The second hook only gets called if the first returns 0. It'll require some further discussion to make it work together.)
Quoted 4 lines
Quoted 4 lines
Right. In that case, would it make sense to pass sk into the new
security_unix_find()hook, perhaps with the new argument namedstruct sock *other? Then we can use this hook for the scope check as well by using landlock_cred(other->sk_socket->file->f_cred)->domain etc.Tingmao,
This seems like the best way to do it. Alternatively, I considered passing in just the
f_credto the hook, but I'm leaning towards a more generic implementation like the one you have below.Quoted 38 lines
Quoted 38 lines
net/unix/af_unix.c +7
-7@@ -1223,24 +1223,24 @@ static struct sock *unix_find_bsd(struct sockaddr_un *sunaddr, int addr_len, err = -ECONNREFUSED ; inode =d_backing_inode (path.dentry);if (!S_ISSOCK (inode->i_mode)) goto path_put; + err = -ECONNREFUSED ; + sk =unix_find_socket_byinode (inode); +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, type, flags); + err =security_unix_find (&path, sk, flags);if (err) - goto path_put; - - err = -ECONNREFUSED ; - sk =unix_find_socket_byinode (inode); -if (!sk) - goto path_put; + goto sock_put; err = -EPROTOTYPE ;if (sk->sk_type == type)touch_atime (&path);else goto sock_put;
Quoted 9 lines
Quoted 7 lines
Quoted 7 lines
By doing this we won't even need to pass
typeseparately anymore. The only change would be that now one can determine if a socket is bound or not even without being allowedRESOLVE_UNIXaccess. I'm not sure how much of an issue this is, but we could also call the hook anyway with a NULL in place of the new argument, ifunix_find_socket_byinode()fails. Other LSMs can then decide what to do in that case (either return-ECONNREFUSEDor-EPERM).I think landlock already allows checking of existence of files even when when the process doesn't have rights on them, so there is precedent for this so in my mind this would probably be OK.
I agree, there are a lot of different side channels in Linux. Landlock is about access control, like most LSMs, so this should not be an issue. And if an LSM wants to hide this information, it can also return -ECONNREFUSED or something else.
Quoted 137 lines
Quoted 9 lines
Thank you for the suggestion.
Small caveat is that the LSM interface is very central and we should be careful. We have previously gotten the advice from Paul to design the hooks in an LSM-independent way that ideally reflects the arguments to the
unix_find_bsd()function, and this would now deviate (slightly) from that, but simplifying the implementation for us. In my personal opinion, this might be worth doing the trade-off, if AppArmor people also agree, but we should double check.Gunther:
I'd be happy to send you an updated LSM hook patch with the sock parameter.
Just let me know.
Quoted 87 lines
To keep the discussion of implementation and interface separate, I have raised this question in the pathname-restricted-UNIX patch set thread in [1].
[1] https://lore.kernel.org/all/[email protected]/
Quoted 78 lines
Quoted 64 lines
Quoted 44 lines
Furthermore, an application / Landlock config etc can always opt to not use the scope bit at all, if it "knows" all the locations where the application's sockets would be placed, and just use
RESOLVE_UNIXaccess right (or nothing if it is not supported).(The following is a bit of a side note, not terribly relevant if we're deciding to go with the patch as is.)
Quoted 23 lines
Quoted 14 lines
[...]
Another way to put it is that, if FS-based and scope-based controls interacts in the above proposed way, both mechanisms feel like "poking holes" in the other. But as Mickaël said, one can think of the two mechanisms not as independent controls, but rather as two interfaces for the same control. The socket access control is "enabled" if either theLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess is handled, or the scope bit proposed in this patch is enabled.With that said, I can think of some alternative ways that might make this API look "better" (from a subjective point of view, feedback welcome), however it does mean more delays, and specifically, these will depend on
LANDLOCK_ACCESS_FS_RESOLVE_UNIX:Quoted 9 lines
One possibility is to simply always allow a Landlock domain to connect to its own sockets (in the case where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled, otherwise all sockets are allowed). This might be reasonable, as one can only connect to a socket it creates if it has the permission to create it in the first place, which is already controlled byLANDLOCK_ACCESS_FS_MAKE_SOCK, so we don't really lose any policy flexibility here - if for some reason the sandboxer don't want to allow access to any (pathname) sockets, even the sandboxed app's own ones, it can just not allowLANDLOCK_ACCESS_FS_MAKE_SOCKanywhere.Quoted 3 lines
LANDLOCK_ACCESS_FS_MAKE_SOCKis only required to bind/listen to a socket, not to connect. I guess you was thinking aboutLANDLOCK_ACCESS_FS_RESOLVE_UNIXin this case?In this "allow same-scope connect unconditionally" proposal, the application would still be able to (bind to and) connect to its own sockets, even if
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled and nothing is allowed to haveLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess. But a sandboxer which for whatever reason doesn't want this "allow same scope" default can still prevent the use of (pathname) sockets by restrictingLANDLOCK_ACCESS_FS_MAKE_SOCK, because if an app can't connect to any sockets it doesn't own, and can't create any sockets itself either, then it effectively can't connect to any sockets at all.Quoted 4 lines
(Although on second thought, I guess there could be a case where an app first creates some socket files before doing
landlock_restrict_self(), then it might still be able to bind to these even withoutLANDLOCK_ACCESS_FS_MAKE_SOCK?)FWIW, I also really liked Tingmao's first of the two listed possibilities in [1], where she proposed to introduce both rights together. In my understanding, the arguments we have discussed so far for that are:
IN FAVOR:
(pro1) Connecting to a UNIX socket in the same scope is always safe,
and it makes it possible to use named UNIX sockets between the processes within a Landlock domains. (Mickaël convinced me in discussion at FOSDEM that this is true.)If someone absolutely does not want that, they can restrict
LANDLOCK_ACCESS_FS_MAKE_SOCKand achieve the same effect (as Tingmao said above).Quoted 5 lines
(pro2) The implementation of this is simpler.
(I attempted to understand how the "or" semantics would be implemented, and I found it non-trivial when you try to do it for all layers at once. (Kernighan's Law applies, IMHO))
I think the logic would basically be:
1. if any layers deny the access due to handled
RESOLVE_UNIXbut does not have the scope bit set, then we will deny rightaway, without callingdomain_is_scoped().2. Call
domain_is_scoped()with a bitmask of "rules_covered" layers where there areRESOLVE_UNIXrules covering the socket being accessed, and essentially ignore those layers in the scope violation check.Tingmao:
For connecting a pathname unix socket, the order of the hooks landlock sees is something like:
1.
security_unix_find. (to look up the paths)2.
security_unix_may_send,security_unix_stream_connect(after the path is looked up)Which for is called in DGRAM:
unix_dgram_connect OR
unix_dgram_sendmsgand for STREAM:
unix_stream_connect
IIRC, the path lookup only occurs in this order always, so in the logic as you have it the
domain_is_scoped()would be called twice, once from thesecurity_unix_findwhen you call it in step two, and once from the domain scope hooks. (If access was allowed fromsecurity_unix_find)There are a couple of things to consider.
---
Audit blockers need special handling:
Here's an example:
1. Program A enforces a ruleset with
RESOLVE_UNIXand the unix pathname scope bit, with no rules with that access bit (deny all forRESOLVE_UNIX).2. Program A connects to
/tmp/mysock.sockran by program B, which is outside the domain.
Quoted 7 lines
2.
security_unix_findis hit to lookup the path, and the RESOLVE_UNIX code doesn't grant access to/tmp/mysock.sock, so it callsdomain_is_scoped3.
domain_is_scopeddenies it as well, so now we must log an audit record.When logging the denial, we have to include both blockers "scope.unix_socket" and "fs.resolve_unix" for the
denial, because it is the absence of both that caused the denial. I think the refer right has similar cases for auditing, so there is precedent for this (multiple blockers for an audit message).
That's a good point, and it would give more informations to diagnose issues. However, being able to identify if both accesses are denied would require to check both, whereas the first is enough to know that Landlock denies the access. So, if we can return both records without continuing the security checks, that's good, otherwise we should stop ASAP and return the error.
Anyway, that might not be needed if we end up with my latest proposal about always setting scope.unix_socket when fs.resolve_unix is set.
Quoted 191 lines
---
Dual lookup for
domain_is_scoped. Consider this case:1. Program A enforces a ruleset with
RESOLVE_UNIXand the unix pathname scope bit, with no rules with that access bit (deny all forRESOLVE_UNIX).2. Program A connects to Program C's
/tmp/foo.sock, which for the purposes of this example is in the domain of program A.3.
security_unix_findis hit to lookup the path, and the RESOLVE_UNIX code doesn't grant access to/tmp/mysock.sock, so it callsdomain_is_scoped. Access is granted, and continues. (LSM hook complete)4. The connection proceeds past the path lookup stage, and now security_unix_may_send, or
security_unix_stream_connectgets called. This requires ANOTHERdomain_is_scopedaccess check.While I don't THINK this introduces a TOCTOU, it is a little confusing.
This does mean that we look up the domain twice, if this is implemented naively. I think we can then just skip the task credential checks then for
security_unix_may_sendandsecurity_unix_stream_connect**for connecting to pathname sockets**, since thedomain_is_scopedwill already have been called in landlock'ssecurity_unix_findhook, eliminating the need for handling pathname socket domain checks layer on.Quoted 104 lines
Quoted 7 lines
I definitely agree that it is tricky, but making same-scope access be allowed (i.e. the suggested idea above) would only get rid of step 1, which I think is the "simpler" bit. The extra logic in step 2 is still needed.
I definitely agree with pro1 tho.
Yes, you are describing the logic for the variant where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXdoes not implicitly permit access from within the same scope. In that variant, there can be situations where the first hook can deny the action immediately.In the variant where
LANDLOCK_ACCESS_FS_RESOLVE_UNIX*does* implicitly allow access from within the same scope, that shortcutting is not possible. On the upside however, there is no need to distinguish whether the scope flag is set when we are in thesecurity_unix_find()hook, because access from within the same scope is always permitted. (That is the simplification I meant.)Quoted 24 lines
Quoted 14 lines
AGAINST:
(con1) It would work differently than the other scoped access rights
that we already have.A speculative feature that could potentially be built with the scoped access rights is that we could add a rule to permit IPC to other Landlock scopes, e.g. introducing a new rule type
struct
landlock_scope_attr{
__u64allowed_access; /* for "scoped" bits */ /* some way to identify domains */
}Quoted 6 lines
so that we could make IPC access to other Landlock domains configurable.
If the scoped bit and the FS
RESOLVE_UNIXbit were both conflated inRESOLVE_UNIX, it would not be possible to make UNIX connections configurable in such a way.This exact API would no longer work, but if we give up the equivalence between scope bits and the
landlock_scope_attrstruct, then we can do something like:Quoted 13 lines
struct landlock_scope_attr { __u64 ptrace:1; /* Note that this is not a (user controllable) scope bit! */ __u64 abstract_unix_socket:1; __u64 pathname_unix_socket:1; /* ... */ __u64 allowed_signals; /* * some way to identify domains, maybe we could use the audit domain * ID, with 0 denoting "allow access to non-Landlocked processes? */ }Yes, it would be possible to use such a struct for that scenario where IPC access gets allowed for other Landlock scopes. It would mean that we would not need to introduce a scoped flag for the pathname UNIX socket connections. But the relationship between that struct
landlock_scope_attrand the flags and access rights in structlandlock_ruleset_attrwould become less clear, which is a slight downside, and maybe error prone for users to work with.If we introduced an additional scoped flag, it would also be consistent though.
(con1) was written under the assumption that we do not have an additional scoped flag. If that is lacking, it is not possible to express UNIX
connect()access to other Landlock domains with that struct. But as outlined in the proposal below, if we *do* (later) introduce the additional scoped flag *in addition* to the FS access right, this *both* stays consistent in semantics with the signal and abstract UNIX support, *and* it starts working in a world where ICP access can be allowed to talk to other Landlock domains.Quoted 16 lines
Quoted 9 lines
(con2) Consistent behaviour between scoped flags and their
interactions with other access rights:The existing scoped access rights (signal, abstract sockets) could hypothetically be extended with a related access right of another type. For instance, there could be an access right type
__u64handled_signal_number;Quoted 7 lines
and then you could add a rule to permit the use of certain signal numbers. The interaction between the scoped flags and other access rights should work the same.
Constructive Proposal for consideration: Why not both?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Quoted 7 lines
I will think about the following a bit more but I'm afraid that I feel like it might get slightly confusing. With this, the only reason for having
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETis for API consistency when we later enable allowing access to other domains (if I understood correctly), in which case I personally feel like the suggestion onlandlock_scope_attrabove, where we essentially accept that it is decoupled with the scope bits in the ruleset, might be simpler...?Mickaël expressed the opinion to me that he would like to APIs to stay consistent between signals, abstract UNIX sockets, named UNIX sockets and other future "scoped" operations, in scenarios where:
Quoted 62 lines
* the "scoped" (IPC) operations can be configured to give access to other Landlock domains (and that should work for UNIX connections too)
* the existing "scoped" operations also start having matching access rightsI think with the way I proposed, that would be consistent.
Quoted 55 lines
Quoted 55 lines
Why not do both what Tingmao proposed in [1] **and** reserve the option to add the matching "scoped flag" later?
* Introduce
LANDLOCK_ACCESS_FS_RESOLVE_UNIX.If it is handled, UNIX connections are allowed either:
(1) if the connection is to a service in the same scope, or
(2) if the path was allow-listed with a "path beneath" rule.* Add
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETlater, if needed.Let's go through the arguments again:
We have observed that it is harmless to allow connections to services in the same scope (1), and that if users absolutely don't want that, they can actually prohibit it through
LANDLOCK_ACCESS_FS_MAKE_SOCK(pro1).(con1): Can we still implement the feature idea where we poke a hole
to get UNIX-connect() access to other Landlock domains?I think the answer is yes. The implementation strategy is:
* Add the scoped bit
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
* The scoped bit can now be used to allow-list connections to other Landlock domains.For users, just setting the scoped bit on its own does the same as handling
LANDLOCK_ACCESS_FS_RESOLVE_UNIX. That way, the kernel-side implementation can also stay simple. The only reason why the scoped bit is needed is because it makes it possible to allow-list connections to other Landlock domains, but at the same time, it is safe if libraries set the scoped bit once it exists, as it does not have any bad runtime impact either.(con2): Consistency: Do all the scoped flags interact with their
corresponding access rights in the same way?The other scope flags do not have corresponding access rights, so far.
If we were to add corresponding access rights for the other scope flags, I would argue that we could apply a consistent logic there, because IPC access within the same scope is always safe:
- A hypothetical access right type for "signal numbers" would only restrict signals that go beyond the current scope.
- A hypothetical access right type for "abstract UNIX domain socket names" would only restrict connections to abstract UNIX domain servers that go beyond the current scope.
I can not come up with a scenario where this doesn't work.
Gunther / Tingmao / Mickaël:
Quoted 8 lines
I have a potential idea to make this concept cleaner.
The docs for landlock currently say:
IPC scoping does not support exceptions via
landlock_add_rule(2). If an operation is scoped within a domain, no rules can be added to allow access to resources or processes outside of the scope.
This part might indeed be confusing. The idea was to explain the difference between scoped rights and handled access rights (which may have rules).
Quoted 5 lines
So if we go with the solution where we are now saying IPC scoping DOES support exceptions we will need to update the documentation, to say scoping for pathname unix sockets is an exception, and have to have the "exemptible scopes" (like this one) alongside "non-exemptible" scopes (ie the existing ones). This creates some friction for users.
The documentation will definitely require some updates. I think it can be explained in a simple way.
Quoted 32 lines
If we foresee other "exempt-able scopes" (which are scopes that also support creating exemptions w/ corresponding access rights) in the future, maybe we should consider separating the two in the ruleset attributes (I used
scoped_fsas an example for the attribute name):structlandlock_ruleset_attrruleset_attr={
.handled_access_fs=
LANDLOCK_ACCESS_FS_EXECUTE|
LANDLOCK_ACCESS_FS_WRITE_FILE|
LANDLOCK_ACCESS_FS_READ_FILE|
LANDLOCK_ACCESS_FS_READ_DIR|
LANDLOCK_ACCESS_FS_REMOVE_DIR|
LANDLOCK_ACCESS_FS_REMOVE_FILE|
LANDLOCK_ACCESS_FS_MAKE_CHAR|
LANDLOCK_ACCESS_FS_MAKE_DIR|
LANDLOCK_ACCESS_FS_MAKE_REG|
LANDLOCK_ACCESS_FS_MAKE_SOCK|
LANDLOCK_ACCESS_FS_MAKE_FIFO|
LANDLOCK_ACCESS_FS_MAKE_BLOCK|
LANDLOCK_ACCESS_FS_MAKE_SYM|
LANDLOCK_ACCESS_FS_REFER|
LANDLOCK_ACCESS_FS_TRUNCATE|LANDLOCK_ACCESS_FS_IOCTL_DEV,
.handled_access_net=
LANDLOCK_ACCESS_NET_BIND_TCP|LANDLOCK_ACCESS_NET_CONNECT_TCP,
.scoped=
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET|LANDLOCK_SCOPE_SIGNAL,
.scoped_fs =
LANDLOCK_SCOPE_FS_PATHNAME_UNIX_SOCKET
};
Quoted 4 lines
This more clearly distinguishes between scopes that have exceptions/corresponding fs rights, and ones that don't. Later we could add
scoped_net, if needed. I feel like this would be more intuitive and better categorize future scoping rights. An obvious con is increasing the size of the ruleset attributes.
I see your point but I don't think it would be worth it to add sub-scoped fields. Each field has a clear semantic, and the scoped one is related to the domain wrt other domains.
Quoted 4 lines
Of course this separation is only worth it if there are other "exempt-able" rights in the future. I can think of a few potential future rights which COULD be scoped and have corresponding rights (binder, sysv-ipc, pipes, tcp/udp between two local programs).
Yes, it would definitely be useful to add exception for other kind of IPCs. The idea would be to be able to describe the peer, either with a file path, or PID, or cgroups, or a Landlock domain... The inet case is an interesting idea but that might be a challenging task to implement, if even possible.
Quoted 24 lines
Quoted 23 lines
Quoted 17 lines
Quoted 17 lines
In conclusion, I think the approach has significant upsides:
* Simpler UAPI: Users only have one access bit to deal with, in the near future. Once we do add a scope flag for UNIX connections, it does not interact in a surprising way with the corresponding FS access right, because with either of these, scoped access is allowed.
If users absolutely need to restrict scoped access, they can restrict
LANDLOCK_ACCESS_FS_MAKE_SOCK. It is a slightly obscure API, but in line with the "make easy things easy, make hard things possible" API philosophy. And needing this should be the exception rather than the norm, after all.Quoted 5 lines
Quoted 5 lines
* Consistent behaviour between scoped flags and regular access rights, also for speculative access rights affecting the existing scoped flags for signals and abstract UNIX domain sockets.
—Günther
Quoted 39 lines · Mickaël Salaün
Quoted 39 lines
[...]
Tingmao:For connecting a pathname unix socket, the order of the hooks landlock sees is something like:
1.
security_unix_find. (to look up the paths)2.
security_unix_may_send,security_unix_stream_connect(after the path is looked up)Which for is called in DGRAM:
unix_dgram_connectORunix_dgram_sendmsgand for STREAM:
unix_stream_connectIIRC, the path lookup only occurs in this order always, so in the logic as you have it the
domain_is_scoped()would be called twice, once from thesecurity_unix_findwhen you call it in step two, and once from the domain scope hooks. (If access was allowed fromsecurity_unix_find)There are a couple of things to consider.
---
Audit blockers need special handling:
Here's an example:
1. Program A enforces a ruleset with
RESOLVE_UNIXand the unix pathname scope bit, with no rules with that access bit (deny all forRESOLVE_UNIX).2. Program A connects to
/tmp/mysock.sockran by program B, which is outside the domain.2.
security_unix_findis hit to lookup the path, and theRESOLVE_UNIXcode doesn't grant access to/tmp/mysock.sock, so it callsdomain_is_scoped3.
domain_is_scopeddenies it as well, so now we must log an audit record.
Quoted 8 lines
Quoted 2 lines
When logging the denial, we have to include both blockers "scope.unix_socket" and "fs.resolve_unix" for the
denial, because it is the absence of both that caused the denial. I think the refer right has similar cases for auditing, so there is precedent for this (multiple blockers for an audit message).That's a good point, and it would give more informations to diagnose issues. However, being able to identify if both accesses are denied would require to check both, whereas the first is enough to know that Landlock denies the access. So, if we can return both records without continuing the security checks, that's good, otherwise we should stop ASAP and return the error.
Maybe I'm missing something, but if the flags interact in an "OR" manner wouldn't we need to check both? In your proposal where RESOLVE_UNIX implies the scoped flag, if a program connects to a unix socket that is within the domain but does not have an explicit RESOLVE_UNIX exception, we must still check for the case that the access is scoped.
---
(Given LANDLOCK_ACCESS_FS_RESOLVE_UNIX and LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET are set)
case 1: access to socket within domain and no RESOLVE_UNIX rule covers the access
We check first in security_unix_find hook and find there is no rule allowing the access. After the check fails, because LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET is set we then check is_domain_scoped, the check allows the access.
case 2: access to socket outside domain but RESOLVE_UNIX rule covers the access
We check first in security_unix_find hook and find there is a rule allowing the access. We can allow the access early (short-circuit eval) without calling is_domain_scoped.
case 4: access to socket inside domain and RESOLVE_UNIX covers the access
We check first in security_unix_find hook and find there is a rule allowing the access. We can allow the access early (short-circuit eval) without calling is_domain_scoped. (same as case 2)
case 4: access to socket outside domain and no RESOLVE_UNIX covers the access
We check first in security_unix_find hook and find there is no rule allowing the access. After the check fails, because LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET is set
we then check is_domain_scoped, the check does not allow the access. (it is the combination of the two checks failing that denied the access).
---
Case 4 is what I'm specifically considering would need to have both blockers listed in a denial audit. We can't short circuit in that case because we have to check the scoping before denying. Let me know if I'm misunderstanding this.
(PS: IIRC the hooks used by the LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
will never be hit if the check in security_unix_find fails. So some logic to check this access in security_unix_find will be needed).
Quoted 241 lines
Anyway, that might not be needed if we end up with my latest proposal about always setting scope.unix_socket when fs.resolve_unix is set.
Quoted 188 lines
---
Dual lookup for
domain_is_scoped. Consider this case:1. Program A enforces a ruleset with
RESOLVE_UNIXand the unix pathname scope bit, with no rules with that access bit (deny all forRESOLVE_UNIX).2. Program A connects to Program C's
/tmp/foo.sock, which for the purposes of this example is in the domain of program A.3.
security_unix_findis hit to lookup the path, and theRESOLVE_UNIXcode doesn't grant access to/tmp/mysock.sock, so it callsdomain_is_scoped. Access is granted, and continues. (LSM hook complete)4. The connection proceeds past the path lookup stage, and now
security_unix_may_send, orsecurity_unix_stream_connectgets called. This requires ANOTHERdomain_is_scopedaccess check.While I don't THINK this introduces a TOCTOU, it is a little confusing.
This does mean that we look up the domain twice, if this is implemented naively. I think we can then just skip the task credential checks then for
security_unix_may_sendandsecurity_unix_stream_connect**for connecting to pathname sockets**, since thedomain_is_scopedwill already have been called in landlock'ssecurity_unix_findhook, eliminating the need for handling pathname socket domain checks layer on.Quoted 103 lines
Quoted 6 lines
I definitely agree that it is tricky, but making same-scope access be allowed (i.e. the suggested idea above) would only get rid of step 1, which I think is the "simpler" bit. The extra logic in step 2 is still needed.
I definitely agree with pro1 tho.
Yes, you are describing the logic for the variant where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXdoes not implicitly permit access from within the same scope. In that variant, there can be situations where the first hook can deny the action immediately.In the variant where
LANDLOCK_ACCESS_FS_RESOLVE_UNIX*does* implicitly allow access from within the same scope, that shortcutting is not possible. On the upside however, there is no need to distinguish whether the scope flag is set when we are in thesecurity_unix_find()hook, because access from within the same scope is always permitted. (That is the simplification I meant.)Quoted 24 lines
Quoted 14 lines
AGAINST:
(con1) It would work differently than the other scoped access rights
that we already have.A speculative feature that could potentially be built with the scoped access rights is that we could add a rule to permit IPC to other Landlock scopes, e.g. introducing a new rule type
struct
landlock_scope_attr{
__u64allowed_access; /* for "scoped" bits */ /* some way to identify domains */
}Quoted 6 lines
so that we could make IPC access to other Landlock domains configurable.
If the scoped bit and the FS
RESOLVE_UNIXbit were both conflated inRESOLVE_UNIX, it would not be possible to make UNIX connections configurable in such a way.This exact API would no longer work, but if we give up the equivalence between scope bits and the
landlock_scope_attrstruct, then we can do something like:Quoted 13 lines
struct landlock_scope_attr { __u64 ptrace:1; /* Note that this is not a (user controllable) scope bit! */ __u64 abstract_unix_socket:1; __u64 pathname_unix_socket:1; /* ... */ __u64 allowed_signals; /* * some way to identify domains, maybe we could use the audit domain * ID, with 0 denoting "allow access to non-Landlocked processes? */ }Yes, it would be possible to use such a struct for that scenario where IPC access gets allowed for other Landlock scopes. It would mean that we would not need to introduce a scoped flag for the pathname UNIX socket connections. But the relationship between that struct
landlock_scope_attrand the flags and access rights in structlandlock_ruleset_attrwould become less clear, which is a slight downside, and maybe error prone for users to work with.If we introduced an additional scoped flag, it would also be consistent though.
(con1) was written under the assumption that we do not have an additional scoped flag. If that is lacking, it is not possible to express UNIX
connect()access to other Landlock domains with that struct. But as outlined in the proposal below, if we *do* (later) introduce the additional scoped flag *in addition* to the FS access right, this *both* stays consistent in semantics with the signal and abstract UNIX support, *and* it starts working in a world where ICP access can be allowed to talk to other Landlock domains.Quoted 16 lines
Quoted 9 lines
(con2) Consistent behaviour between scoped flags and their
interactions with other access rights:The existing scoped access rights (signal, abstract sockets) could hypothetically be extended with a related access right of another type. For instance, there could be an access right type
__u64handled_signal_number;Quoted 7 lines
and then you could add a rule to permit the use of certain signal numbers. The interaction between the scoped flags and other access rights should work the same.
Constructive Proposal for consideration: Why not both?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Quoted 7 lines
I will think about the following a bit more but I'm afraid that I feel like it might get slightly confusing. With this, the only reason for having
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETis for API consistency when we later enable allowing access to other domains (if I understood correctly), in which case I personally feel like the suggestion onlandlock_scope_attrabove, where we essentially accept that it is decoupled with the scope bits in the ruleset, might be simpler...?Mickaël expressed the opinion to me that he would like to APIs to stay consistent between signals, abstract UNIX sockets, named UNIX sockets and other future "scoped" operations, in scenarios where:
Quoted 62 lines
* the "scoped" (IPC) operations can be configured to give access to other Landlock domains (and that should work for UNIX connections too)
* the existing "scoped" operations also start having matching access rightsI think with the way I proposed, that would be consistent.
Quoted 55 lines
Quoted 55 lines
Why not do both what Tingmao proposed in [1] **and** reserve the option to add the matching "scoped flag" later?
* Introduce
LANDLOCK_ACCESS_FS_RESOLVE_UNIX.If it is handled, UNIX connections are allowed either:
(1) if the connection is to a service in the same scope, or
(2) if the path was allow-listed with a "path beneath" rule.* Add
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETlater, if needed.Let's go through the arguments again:
We have observed that it is harmless to allow connections to services in the same scope (1), and that if users absolutely don't want that, they can actually prohibit it through
LANDLOCK_ACCESS_FS_MAKE_SOCK(pro1).(con1): Can we still implement the feature idea where we poke a hole
to get UNIX-connect() access to other Landlock domains?I think the answer is yes. The implementation strategy is:
* Add the scoped bit
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
* The scoped bit can now be used to allow-list connections to other Landlock domains.For users, just setting the scoped bit on its own does the same as handling
LANDLOCK_ACCESS_FS_RESOLVE_UNIX. That way, the kernel-side implementation can also stay simple. The only reason why the scoped bit is needed is because it makes it possible to allow-list connections to other Landlock domains, but at the same time, it is safe if libraries set the scoped bit once it exists, as it does not have any bad runtime impact either.(con2): Consistency: Do all the scoped flags interact with their
corresponding access rights in the same way?The other scope flags do not have corresponding access rights, so far.
If we were to add corresponding access rights for the other scope flags, I would argue that we could apply a consistent logic there, because IPC access within the same scope is always safe:
- A hypothetical access right type for "signal numbers" would only restrict signals that go beyond the current scope.
- A hypothetical access right type for "abstract UNIX domain socket names" would only restrict connections to abstract UNIX domain servers that go beyond the current scope.
I can not come up with a scenario where this doesn't work.
Gunther / Tingmao / Mickaël:
Quoted 8 lines
I have a potential idea to make this concept cleaner.
The docs for landlock currently say:
IPC scoping does not support exceptions via
landlock_add_rule(2). If an operation is scoped within a domain, no rules can be added to allow access to resources or processes outside of the scope.This part might indeed be confusing. The idea was to explain the difference between scoped rights and handled access rights (which may have rules).
Quoted 4 lines
So if we go with the solution where we are now saying IPC scoping DOES support exceptions we will need to update the documentation, to say scoping for pathname unix sockets is an exception, and have to have the "exemptible scopes" (like this one) alongside "non-exemptible" scopes (ie the existing ones). This creates some friction for users.
The documentation will definitely require some updates. I think it can be explained in a simple way.
Quoted 31 lines
If we foresee other "exempt-able scopes" (which are scopes that also support creating exemptions w/ corresponding access rights) in the future, maybe we should consider separating the two in the ruleset attributes (I used
scoped_fsas an example for the attribute name):structlandlock_ruleset_attrruleset_attr={
.handled_access_fs=
LANDLOCK_ACCESS_FS_EXECUTE|
LANDLOCK_ACCESS_FS_WRITE_FILE|
LANDLOCK_ACCESS_FS_READ_FILE|
LANDLOCK_ACCESS_FS_READ_DIR|
LANDLOCK_ACCESS_FS_REMOVE_DIR|
LANDLOCK_ACCESS_FS_REMOVE_FILE|
LANDLOCK_ACCESS_FS_MAKE_CHAR|
LANDLOCK_ACCESS_FS_MAKE_DIR|
LANDLOCK_ACCESS_FS_MAKE_REG|
LANDLOCK_ACCESS_FS_MAKE_SOCK|
LANDLOCK_ACCESS_FS_MAKE_FIFO|
LANDLOCK_ACCESS_FS_MAKE_BLOCK|
LANDLOCK_ACCESS_FS_MAKE_SYM|
LANDLOCK_ACCESS_FS_REFER|
LANDLOCK_ACCESS_FS_TRUNCATE|LANDLOCK_ACCESS_FS_IOCTL_DEV,
.handled_access_net=
LANDLOCK_ACCESS_NET_BIND_TCP|LANDLOCK_ACCESS_NET_CONNECT_TCP,
.scoped=
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET|LANDLOCK_SCOPE_SIGNAL,
.scoped_fs =
LANDLOCK_SCOPE_FS_PATHNAME_UNIX_SOCKET
};
Quoted 7 lines
Quoted 4 lines
This more clearly distinguishes between scopes that have exceptions/corresponding fs rights, and ones that don't. Later we could add
scoped_net, if needed. I feel like this would be more intuitive and better categorize future scoping rights. An obvious con is increasing the size of the ruleset attributes.I see your point but I don't think it would be worth it to add sub-scoped fields. Each field has a clear semantic, and the scoped one is related to the domain wrt other domains.
As long as it's documented clearly, and future IPCs have similar behavior I agree that a separate field probably isn't needed.
Quoted 29 lines
Quoted 3 lines
Of course this separation is only worth it if there are other "exempt-able" rights in the future. I can think of a few potential future rights which COULD be scoped and have corresponding rights (binder, sysv-ipc, pipes, tcp/udp between two local programs).
Yes, it would definitely be useful to add exception for other kind of IPCs. The idea would be to be able to describe the peer, either with a file path, or PID, or cgroups, or a Landlock domain... The inet case is an interesting idea but that might be a challenging task to implement, if even possible.
Quoted 20 lines
Quoted 20 lines
Quoted 14 lines
Quoted 14 lines
In conclusion, I think the approach has significant upsides:
* Simpler UAPI: Users only have one access bit to deal with, in the near future. Once we do add a scope flag for UNIX connections, it does not interact in a surprising way with the corresponding FS access right, because with either of these, scoped access is allowed.
If users absolutely need to restrict scoped access, they can restrict
LANDLOCK_ACCESS_FS_MAKE_SOCK. It is a slightly obscure API, but in line with the "make easy things easy, make hard things possible" API philosophy. And needing this should be the exception rather than the norm, after all.Quoted 5 lines
Quoted 5 lines
* Consistent behaviour between scoped flags and regular access rights, also for speculative access rights affecting the existing scoped flags for signals and abstract UNIX domain sockets.
—Günther
A message that is not in the archive (sent off-list or not yet received): [email protected]
Mickaël Salaün
maintainerQuoted 44 lines · Justin Suess
Quoted 41 lines · Mickaël Salaün
Quoted 41 lines
[...]
Tingmao:For connecting a pathname unix socket, the order of the hooks landlock sees is something like:
1.
security_unix_find. (to look up the paths)2.
security_unix_may_send,security_unix_stream_connect(after the path is looked up)Which for is called in DGRAM:
unix_dgram_connect OR
unix_dgram_sendmsgand for STREAM:
unix_stream_connect
IIRC, the path lookup only occurs in this order always, so in the logic as you have it the
domain_is_scoped()would be called twice, once from thesecurity_unix_findwhen you call it in step two, and once from the domain scope hooks. (If access was allowed fromsecurity_unix_find)There are a couple of things to consider.
---
Audit blockers need special handling:
Here's an example:
1. Program A enforces a ruleset with
RESOLVE_UNIXand the unix pathname scope bit, with no rules with that access bit (deny all forRESOLVE_UNIX).2. Program A connects to
/tmp/mysock.sockran by program B, which is outside the domain.2.
security_unix_findis hit to lookup the path, and the RESOLVE_UNIX code doesn't grant access to/tmp/mysock.sock, so it callsdomain_is_scoped3.
domain_is_scopeddenies it as well, so now we must log an audit record.When logging the denial, we have to include both blockers "scope.unix_socket" and "fs.resolve_unix" for the
denial, because it is the absence of both that caused the denial. I think the refer right has similar cases for auditing, so there is precedent for this (multiple blockers for an audit message).
Quoted 8 lines
Quoted 6 lines
That's a good point, and it would give more informations to diagnose issues. However, being able to identify if both accesses are denied would require to check both, whereas the first is enough to know that Landlock denies the access. So, if we can return both records without continuing the security checks, that's good, otherwise we should stop ASAP and return the error.
Maybe I'm missing something, but if the flags interact in an "OR" manner wouldn't we need to check both?
Yes, but my point is that as soon as one or the other *denies* an access, there is no need to check the other access type.
Quoted 27 lines
In your proposal where
RESOLVE_UNIX
implies the scoped flag, if a program connects to a unix socket that is within the domain but does not have an explicitRESOLVE_UNIXexception, we must still check for the case that the access is scoped.---
(Given
LANDLOCK_ACCESS_FS_RESOLVE_UNIXandLANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETare set)case 1: access to socket within domain and no
RESOLVE_UNIXrule covers the accessWe check first in
security_unix_findhook and find there is no rule allowing the access. After the check fails, becauseLANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETis set we then checkis_domain_scoped, the check allows the access.case 2: access to socket outside domain but
RESOLVE_UNIXrule covers the accessWe check first in
security_unix_findhook and find there is a rule allowing the access. We can allow the access early (short-circuit eval) without callingis_domain_scoped.case 4: access to socket inside domain and
RESOLVE_UNIXcovers the accessWe check first in
security_unix_findhook and find there is a rule allowing the access. We can allow the access early (short-circuit eval) without callingis_domain_scoped. (same as case 2)
Quoted 6 lines
case 4: access to socket outside domain and no
RESOLVE_UNIXcovers the accessWe check first in
security_unix_findhook and find there is no rule allowing the access. After the check fails, becauseLANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETis set
we then checkis_domain_scoped, the check does not allow the access. (it is the combination of the two checks failing that denied the access).
BTW, we should first check is_domain_scoped() because it is quicker than the FS checks.
Quoted 6 lines
---
Case 4 is what I'm specifically considering would need to have both blockers listed in a denial audit. We can't short circuit in that case because we have to check the scoping before denying. Let me know if I'm misunderstanding this.
Indeed, if both deny the request, both should be listed.
Anyway, we're now going to only have one access flag that would merge both semantic, so this should not be an issue.
However, this specific case will be relevant when we'll add e.g., a signal attr that would then be complementary to the signal scope. At this point, I think it would be enough to only log the signal.* record because it would be a superset of the scope.signal
Quoted 288 lines
(PS: IIRC the hooks used by the
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
will never be hit if the check insecurity_unix_findfails. So some logic to check this access insecurity_unix_findwill be needed).Quoted 242 lines
Anyway, that might not be needed if we end up with my latest proposal about always setting scope.unix_socket when fs.resolve_unix is set.
Quoted 188 lines
---
Dual lookup for
domain_is_scoped. Consider this case:1. Program A enforces a ruleset with
RESOLVE_UNIXand the unix pathname scope bit, with no rules with that access bit (deny all forRESOLVE_UNIX).2. Program A connects to Program C's
/tmp/foo.sock, which for the purposes of this example is in the domain of program A.3.
security_unix_findis hit to lookup the path, and the RESOLVE_UNIX code doesn't grant access to/tmp/mysock.sock, so it callsdomain_is_scoped. Access is granted, and continues. (LSM hook complete)4. The connection proceeds past the path lookup stage, and now security_unix_may_send, or
security_unix_stream_connectgets called. This requires ANOTHERdomain_is_scopedaccess check.While I don't THINK this introduces a TOCTOU, it is a little confusing.
This does mean that we look up the domain twice, if this is implemented naively. I think we can then just skip the task credential checks then for
security_unix_may_sendandsecurity_unix_stream_connect**for connecting to pathname sockets**, since thedomain_is_scopedwill already have been called in landlock'ssecurity_unix_findhook, eliminating the need for handling pathname socket domain checks layer on.Quoted 103 lines
Quoted 6 lines
I definitely agree that it is tricky, but making same-scope access be allowed (i.e. the suggested idea above) would only get rid of step 1, which I think is the "simpler" bit. The extra logic in step 2 is still needed.
I definitely agree with pro1 tho.
Yes, you are describing the logic for the variant where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXdoes not implicitly permit access from within the same scope. In that variant, there can be situations where the first hook can deny the action immediately.In the variant where
LANDLOCK_ACCESS_FS_RESOLVE_UNIX*does* implicitly allow access from within the same scope, that shortcutting is not possible. On the upside however, there is no need to distinguish whether the scope flag is set when we are in thesecurity_unix_find()hook, because access from within the same scope is always permitted. (That is the simplification I meant.)Quoted 24 lines
Quoted 14 lines
AGAINST:
(con1) It would work differently than the other scoped access rights
that we already have.A speculative feature that could potentially be built with the scoped access rights is that we could add a rule to permit IPC to other Landlock scopes, e.g. introducing a new rule type
struct
landlock_scope_attr{
__u64allowed_access; /* for "scoped" bits */ /* some way to identify domains */
}Quoted 6 lines
so that we could make IPC access to other Landlock domains configurable.
If the scoped bit and the FS
RESOLVE_UNIXbit were both conflated inRESOLVE_UNIX, it would not be possible to make UNIX connections configurable in such a way.This exact API would no longer work, but if we give up the equivalence between scope bits and the
landlock_scope_attrstruct, then we can do something like:Quoted 13 lines
struct landlock_scope_attr { __u64 ptrace:1; /* Note that this is not a (user controllable) scope bit! */ __u64 abstract_unix_socket:1; __u64 pathname_unix_socket:1; /* ... */ __u64 allowed_signals; /* * some way to identify domains, maybe we could use the audit domain * ID, with 0 denoting "allow access to non-Landlocked processes? */ }Yes, it would be possible to use such a struct for that scenario where IPC access gets allowed for other Landlock scopes. It would mean that we would not need to introduce a scoped flag for the pathname UNIX socket connections. But the relationship between that struct
landlock_scope_attrand the flags and access rights in structlandlock_ruleset_attrwould become less clear, which is a slight downside, and maybe error prone for users to work with.If we introduced an additional scoped flag, it would also be consistent though.
(con1) was written under the assumption that we do not have an additional scoped flag. If that is lacking, it is not possible to express UNIX
connect()access to other Landlock domains with that struct. But as outlined in the proposal below, if we *do* (later) introduce the additional scoped flag *in addition* to the FS access right, this *both* stays consistent in semantics with the signal and abstract UNIX support, *and* it starts working in a world where ICP access can be allowed to talk to other Landlock domains.Quoted 16 lines
Quoted 9 lines
(con2) Consistent behaviour between scoped flags and their
interactions with other access rights:The existing scoped access rights (signal, abstract sockets) could hypothetically be extended with a related access right of another type. For instance, there could be an access right type
__u64handled_signal_number;Quoted 7 lines
and then you could add a rule to permit the use of certain signal numbers. The interaction between the scoped flags and other access rights should work the same.
Constructive Proposal for consideration: Why not both?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Quoted 7 lines
I will think about the following a bit more but I'm afraid that I feel like it might get slightly confusing. With this, the only reason for having
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETis for API consistency when we later enable allowing access to other domains (if I understood correctly), in which case I personally feel like the suggestion onlandlock_scope_attrabove, where we essentially accept that it is decoupled with the scope bits in the ruleset, might be simpler...?Mickaël expressed the opinion to me that he would like to APIs to stay consistent between signals, abstract UNIX sockets, named UNIX sockets and other future "scoped" operations, in scenarios where:
Quoted 62 lines
* the "scoped" (IPC) operations can be configured to give access to other Landlock domains (and that should work for UNIX connections too)
* the existing "scoped" operations also start having matching access rightsI think with the way I proposed, that would be consistent.
Quoted 55 lines
Quoted 55 lines
Why not do both what Tingmao proposed in [1] **and** reserve the option to add the matching "scoped flag" later?
* Introduce
LANDLOCK_ACCESS_FS_RESOLVE_UNIX.If it is handled, UNIX connections are allowed either:
(1) if the connection is to a service in the same scope, or
(2) if the path was allow-listed with a "path beneath" rule.* Add
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETlater, if needed.Let's go through the arguments again:
We have observed that it is harmless to allow connections to services in the same scope (1), and that if users absolutely don't want that, they can actually prohibit it through
LANDLOCK_ACCESS_FS_MAKE_SOCK(pro1).(con1): Can we still implement the feature idea where we poke a hole
to get UNIX-connect() access to other Landlock domains?I think the answer is yes. The implementation strategy is:
* Add the scoped bit
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
* The scoped bit can now be used to allow-list connections to other Landlock domains.For users, just setting the scoped bit on its own does the same as handling
LANDLOCK_ACCESS_FS_RESOLVE_UNIX. That way, the kernel-side implementation can also stay simple. The only reason why the scoped bit is needed is because it makes it possible to allow-list connections to other Landlock domains, but at the same time, it is safe if libraries set the scoped bit once it exists, as it does not have any bad runtime impact either.(con2): Consistency: Do all the scoped flags interact with their
corresponding access rights in the same way?The other scope flags do not have corresponding access rights, so far.
If we were to add corresponding access rights for the other scope flags, I would argue that we could apply a consistent logic there, because IPC access within the same scope is always safe:
- A hypothetical access right type for "signal numbers" would only restrict signals that go beyond the current scope.
- A hypothetical access right type for "abstract UNIX domain socket names" would only restrict connections to abstract UNIX domain servers that go beyond the current scope.
I can not come up with a scenario where this doesn't work.
Gunther / Tingmao / Mickaël:
Quoted 8 lines
I have a potential idea to make this concept cleaner.
The docs for landlock currently say:
IPC scoping does not support exceptions via
landlock_add_rule(2). If an operation is scoped within a domain, no rules can be added to allow access to resources or processes outside of the scope.This part might indeed be confusing. The idea was to explain the difference between scoped rights and handled access rights (which may have rules).
Quoted 4 lines
So if we go with the solution where we are now saying IPC scoping DOES support exceptions we will need to update the documentation, to say scoping for pathname unix sockets is an exception, and have to have the "exemptible scopes" (like this one) alongside "non-exemptible" scopes (ie the existing ones). This creates some friction for users.
The documentation will definitely require some updates. I think it can be explained in a simple way.
Quoted 31 lines
If we foresee other "exempt-able scopes" (which are scopes that also support creating exemptions w/ corresponding access rights) in the future, maybe we should consider separating the two in the ruleset attributes (I used
scoped_fsas an example for the attribute name):structlandlock_ruleset_attrruleset_attr={
.handled_access_fs=
LANDLOCK_ACCESS_FS_EXECUTE|
LANDLOCK_ACCESS_FS_WRITE_FILE|
LANDLOCK_ACCESS_FS_READ_FILE|
LANDLOCK_ACCESS_FS_READ_DIR|
LANDLOCK_ACCESS_FS_REMOVE_DIR|
LANDLOCK_ACCESS_FS_REMOVE_FILE|
LANDLOCK_ACCESS_FS_MAKE_CHAR|
LANDLOCK_ACCESS_FS_MAKE_DIR|
LANDLOCK_ACCESS_FS_MAKE_REG|
LANDLOCK_ACCESS_FS_MAKE_SOCK|
LANDLOCK_ACCESS_FS_MAKE_FIFO|
LANDLOCK_ACCESS_FS_MAKE_BLOCK|
LANDLOCK_ACCESS_FS_MAKE_SYM|
LANDLOCK_ACCESS_FS_REFER|
LANDLOCK_ACCESS_FS_TRUNCATE|LANDLOCK_ACCESS_FS_IOCTL_DEV,
.handled_access_net=
LANDLOCK_ACCESS_NET_BIND_TCP|LANDLOCK_ACCESS_NET_CONNECT_TCP,
.scoped=
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET|LANDLOCK_SCOPE_SIGNAL,
.scoped_fs =
LANDLOCK_SCOPE_FS_PATHNAME_UNIX_SOCKET
};Quoted 7 lines
Quoted 4 lines
This more clearly distinguishes between scopes that have exceptions/corresponding fs rights, and ones that don't. Later we could add
scoped_net, if needed. I feel like this would be more intuitive and better categorize future scoping rights. An obvious con is increasing the size of the ruleset attributes.I see your point but I don't think it would be worth it to add sub-scoped fields. Each field has a clear semantic, and the scoped one is related to the domain wrt other domains.
As long as it's documented clearly, and future IPCs have similar behavior I agree that a separate field probably isn't needed.
Quoted 32 lines
Quoted 4 lines
Of course this separation is only worth it if there are other "exempt-able" rights in the future. I can think of a few potential future rights which COULD be scoped and have corresponding rights (binder, sysv-ipc, pipes, tcp/udp between two local programs).
Yes, it would definitely be useful to add exception for other kind of IPCs. The idea would be to be able to describe the peer, either with a file path, or PID, or cgroups, or a Landlock domain... The inet case is an interesting idea but that might be a challenging task to implement, if even possible.
Quoted 21 lines
Quoted 21 lines
Quoted 15 lines
Quoted 15 lines
In conclusion, I think the approach has significant upsides:
* Simpler UAPI: Users only have one access bit to deal with, in the near future. Once we do add a scope flag for UNIX connections, it does not interact in a surprising way with the corresponding FS access right, because with either of these, scoped access is allowed.
If users absolutely need to restrict scoped access, they can restrict
LANDLOCK_ACCESS_FS_MAKE_SOCK. It is a slightly obscure API, but in line with the "make easy things easy, make hard things possible" API philosophy. And needing this should be the exception rather than the norm, after all.Quoted 5 lines
Quoted 5 lines
* Consistent behaviour between scoped flags and regular access rights, also for speculative access rights affecting the existing scoped flags for signals and abstract UNIX domain sockets.
—Günther
Quoted 4 lines · Mickaël Salaün
Quoted 3 lines · Justin Suess
On 2/4/26 13:28, Mickaël Salaün wrote:
Quoted 8 lines
Quoted 8 lines
Quoted 8 lines
Quoted 8 lines
[...]
Tingmao:For connecting a pathname unix socket, the order of the hooks landlock sees is something like:
1.
security_unix_find. (to look up the paths)2.
security_unix_may_send,security_unix_stream_connect(after the path is looked up)
btw, ideally for pathname sockets we can leave all the checking in the security_unix_find() hook (as newly proposed, with the struct sock *other param), and not have to e.g. call domain_is_scoped() again in security_unix_may_send and security_unix_stream_connect, right?
(Although if this changes error codes, we might have to "delay" the denial until the may_send/connect hooks...? Hopefully not but not checked.)
Quoted 11 lines · Tingmao Wang
Quoted 9 lines · Mickaël Salaün
Quoted 8 lines · Justin Suess
Quoted 7 lines · Mickaël Salaün
Quoted 7 lines
Tingmao:
For connecting a pathname unix socket, the order of the hooks landlock sees is something like:
1.
security_unix_find. (to look up the paths)2.
security_unix_may_send,security_unix_stream_connect(after the path is looked up)
Quoted 7 lines
btw, ideally for pathname sockets we can leave all the checking in the
security_unix_find()hook (as newly proposed, with the struct sock *other param), and not have to e.g. calldomain_is_scoped()again insecurity_unix_may_sendandsecurity_unix_stream_connect, right?(Although if this changes error codes, we might have to "delay" the denial until the may_send/connect hooks...? Hopefully not but not checked.)
Yes, absolutely. I have had a stab at it and will send it soon.
Justin adopted your suggestion from [1] and created an updated LSM hook patch based on it. With that, I am doing both checks in the security_unix_find() hook, based on the resulting struct sock.
[1] https://lore.kernel.org/all/[email protected]/#t
–Günther
Quoted 11 lines · Tingmao Wang
Quoted 9 lines · Mickaël Salaün
Quoted 8 lines · Justin Suess
Quoted 7 lines · Mickaël Salaün
Quoted 7 lines
Tingmao:
For connecting a pathname unix socket, the order of the hooks landlock sees is something like:
1.
security_unix_find. (to look up the paths)2.
security_unix_may_send,security_unix_stream_connect(after the path is looked up)
Quoted 7 lines
btw, ideally for pathname sockets we can leave all the checking in the
security_unix_find()hook (as newly proposed, with the struct sock *other param), and not have to e.g. calldomain_is_scoped()again insecurity_unix_may_sendandsecurity_unix_stream_connect, right?(Although if this changes error codes, we might have to "delay" the denial until the may_send/connect hooks...? Hopefully not but not checked.)
Thank you, Tingmao!
So far, the selftests that I already had in fs_test.c were straightforward to extend so that they cover the new cases, but I'll definitely have a look through your patch set and see if there are parts that we can reuse or that I missed to cover. Either way, I'll make sure that you'll get appropriate credit for it. :)
–Günther
Mickaël Salaün
maintainerQuoted 66 lines · Günther Noack
Quoted 55 lines · Tingmao Wang
Quoted 11 lines · Günther Noack
BTW, regarding the implementation: To have *OR* semantics for "within scope" and "allow-listed path", the implementation will be non-trivial, and I suspect we won't hit the merge window if we try to get them both in for 7.0. But in my mind, a simple UAPI is more important than trying to make it in time for the next merge window.
(The implementation is difficult because the path-based and scope-based check currently happen in different LSM hooks, and none of the two hooks has enough information to make the decision alone. The second hook only gets called if the first returns 0. It'll require some further discussion to make it work together.)
Right. In that case, would it make sense to pass sk into the new
security_unix_find()hook, perhaps with the new argument namedstruct sock *other? Then we can use this hook for the scope check as well by using landlock_cred(other->sk_socket->file->f_cred)->domain etc.net/unix/af_unix.c +7
-7@@ -1223,24 +1223,24 @@ static struct sock *unix_find_bsd(struct sockaddr_un *sunaddr, int addr_len, err = -ECONNREFUSED ; inode =d_backing_inode (path.dentry);if (!S_ISSOCK (inode->i_mode)) goto path_put; + err = -ECONNREFUSED ; + sk =unix_find_socket_byinode (inode); +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, type, flags); + err =security_unix_find (&path, sk, flags);if (err) - goto path_put; - - err = -ECONNREFUSED ; - sk =unix_find_socket_byinode (inode); -if (!sk) - goto path_put; + goto sock_put; err = -EPROTOTYPE ;if (sk->sk_type == type)touch_atime (&path);else goto sock_put;Quoted 7 lines
By doing this we won't even need to pass
typeseparately anymore. The only change would be that now one can determine if a socket is bound or not even without being allowedRESOLVE_UNIXaccess. I'm not sure how much of an issue this is, but we could also call the hook anyway with a NULL in place of the new argument, ifunix_find_socket_byinode()fails. Other LSMs can then decide what to do in that case (either return-ECONNREFUSEDor-EPERM).Thank you for the suggestion.
Quoted 7 lines
Small caveat is that the LSM interface is very central and we should be careful. We have previously gotten the advice from Paul to design the hooks in an LSM-independent way that ideally reflects the arguments to the
unix_find_bsd()function, and this would now deviate (slightly) from that, but simplifying the implementation for us. In my personal opinion, this might be worth doing the trade-off, if AppArmor people also agree, but we should double check.
I think that's a good idea. The new hook would more generic and make it possible to have more context to take an access control decision, which is better for the LSM framework.
Quoted 104 lines
To keep the discussion of implementation and interface separate, I have raised this question in the pathname-restricted-UNIX patch set thread in [1].
[1] https://lore.kernel.org/all/[email protected]/
Quoted 83 lines
Quoted 67 lines
Quoted 46 lines
Furthermore, an application / Landlock config etc can always opt to not use the scope bit at all, if it "knows" all the locations where the application's sockets would be placed, and just use
RESOLVE_UNIXaccess right (or nothing if it is not supported).(The following is a bit of a side note, not terribly relevant if we're deciding to go with the patch as is.)
Quoted 24 lines
Quoted 14 lines
[...]
Another way to put it is that, if FS-based and scope-based controls interacts in the above proposed way, both mechanisms feel like "poking holes" in the other. But as Mickaël said, one can think of the two mechanisms not as independent controls, but rather as two interfaces for the same control. The socket access control is "enabled" if either theLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess is handled, or the scope bit proposed in this patch is enabled.With that said, I can think of some alternative ways that might make this API look "better" (from a subjective point of view, feedback welcome), however it does mean more delays, and specifically, these will depend on
LANDLOCK_ACCESS_FS_RESOLVE_UNIX:Quoted 9 lines
One possibility is to simply always allow a Landlock domain to connect to its own sockets (in the case where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled, otherwise all sockets are allowed). This might be reasonable, as one can only connect to a socket it creates if it has the permission to create it in the first place, which is already controlled byLANDLOCK_ACCESS_FS_MAKE_SOCK, so we don't really lose any policy flexibility here - if for some reason the sandboxer don't want to allow access to any (pathname) sockets, even the sandboxed app's own ones, it can just not allowLANDLOCK_ACCESS_FS_MAKE_SOCKanywhere.Quoted 3 lines
LANDLOCK_ACCESS_FS_MAKE_SOCKis only required to bind/listen to a socket, not to connect. I guess you was thinking aboutLANDLOCK_ACCESS_FS_RESOLVE_UNIXin this case?In this "allow same-scope connect unconditionally" proposal, the application would still be able to (bind to and) connect to its own sockets, even if
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled and nothing is allowed to haveLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess. But a sandboxer which for whatever reason doesn't want this "allow same scope" default can still prevent the use of (pathname) sockets by restrictingLANDLOCK_ACCESS_FS_MAKE_SOCK, because if an app can't connect to any sockets it doesn't own, and can't create any sockets itself either, then it effectively can't connect to any sockets at all.Quoted 4 lines
(Although on second thought, I guess there could be a case where an app first creates some socket files before doing
landlock_restrict_self(), then it might still be able to bind to these even withoutLANDLOCK_ACCESS_FS_MAKE_SOCK?)FWIW, I also really liked Tingmao's first of the two listed possibilities in [1], where she proposed to introduce both rights together. In my understanding, the arguments we have discussed so far for that are:
IN FAVOR:
(pro1) Connecting to a UNIX socket in the same scope is always safe,
and it makes it possible to use named UNIX sockets between the processes within a Landlock domains. (Mickaël convinced me in discussion at FOSDEM that this is true.)If someone absolutely does not want that, they can restrict
LANDLOCK_ACCESS_FS_MAKE_SOCKand achieve the same effect (as Tingmao said above).Quoted 5 lines
(pro2) The implementation of this is simpler.
(I attempted to understand how the "or" semantics would be implemented, and I found it non-trivial when you try to do it for all layers at once. (Kernighan's Law applies, IMHO))
I think the logic would basically be:
1. if any layers deny the access due to handled
RESOLVE_UNIXbut does not have the scope bit set, then we will deny rightaway, without callingdomain_is_scoped().2. Call
domain_is_scoped()with a bitmask of "rules_covered" layers where there areRESOLVE_UNIXrules covering the socket being accessed, and essentially ignore those layers in the scope violation check.Quoted 6 lines
I definitely agree that it is tricky, but making same-scope access be allowed (i.e. the suggested idea above) would only get rid of step 1, which I think is the "simpler" bit. The extra logic in step 2 is still needed.
I definitely agree with pro1 tho.
Yes, you are describing the logic for the variant where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXdoes not implicitly permit access from within the same scope. In that variant, there can be situations where the first hook can deny the action immediately.
Quoted 6 lines
In the variant where
LANDLOCK_ACCESS_FS_RESOLVE_UNIX*does* implicitly allow access from within the same scope, that shortcutting is not possible. On the upside however, there is no need to distinguish whether the scope flag is set when we are in thesecurity_unix_find()hook, because access from within the same scope is always permitted. (That is the simplification I meant.)
This proposal make sense, improve IPC restriction consistency (by always be able to connect to peers from the same domain), and would simplify some checks. What bother me the most is the implicit scoping induced by the FS_RESOLVE_UNIX handling. A related issue is the mix of checks for the FS_RESOLVE_UNIX implementation, which could make the code more complex, but I don't see a better way. Landlock already has an implicit ptrace scoping but it's not (and should never be) optional.
See my proposal below about implicit LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET. That would not change the kernel checks you described though.
Quoted 52 lines
Quoted 27 lines
Quoted 16 lines
AGAINST:
(con1) It would work differently than the other scoped access rights
that we already have.A speculative feature that could potentially be built with the scoped access rights is that we could add a rule to permit IPC to other Landlock scopes, e.g. introducing a new rule type
struct
landlock_scope_attr{
__u64allowed_access; /* for "scoped" bits */ /* some way to identify domains */
}Quoted 6 lines
so that we could make IPC access to other Landlock domains configurable.
If the scoped bit and the FS
RESOLVE_UNIXbit were both conflated inRESOLVE_UNIX, it would not be possible to make UNIX connections configurable in such a way.This exact API would no longer work, but if we give up the equivalence between scope bits and the
landlock_scope_attrstruct, then we can do something like:Quoted 13 lines
struct landlock_scope_attr { __u64 ptrace:1; /* Note that this is not a (user controllable) scope bit! */ __u64 abstract_unix_socket:1; __u64 pathname_unix_socket:1; /* ... */ __u64 allowed_signals; /* * some way to identify domains, maybe we could use the audit domain * ID, with 0 denoting "allow access to non-Landlocked processes? */ }Yes, it would be possible to use such a struct for that scenario where IPC access gets allowed for other Landlock scopes. It would mean that we would not need to introduce a scoped flag for the pathname UNIX socket connections. But the relationship between that struct
landlock_scope_attrand the flags and access rights in structlandlock_ruleset_attrwould become less clear, which is a slight downside, and maybe error prone for users to work with.If we introduced an additional scoped flag, it would also be consistent though.
Quoted 8 lines
(con1) was written under the assumption that we do not have an additional scoped flag. If that is lacking, it is not possible to express UNIX
connect()access to other Landlock domains with that struct. But as outlined in the proposal below, if we *do* (later) introduce the additional scoped flag *in addition* to the FS access right, this *both* stays consistent in semantics with the signal and abstract UNIX support, *and* it starts working in a world where ICP access can be allowed to talk to other Landlock domains.
In this case, we need to assume the scoped flag is already there, and because we have the implementation, to actually add it now.
As a side note, I don't really like this landlock_scope_attr interface. A dedicated attr per kind of IPC would be much more flexible (e.g. the handled_signal_number you described below). For instance, even if at first we don't make it possible to describe different signals within the attr struct, this struct will be able to grow. So it's better to have a dedicated interface per IPC type than a generic scope interface that can only express a subset of it.
Anyway, this proposal could work for pathname UNIX sockets, but a future signal attr would be a bit inconsistent wrt to the pathname UNIX one.
Quoted 30 lines
Quoted 18 lines
Quoted 10 lines
(con2) Consistent behaviour between scoped flags and their
interactions with other access rights:The existing scoped access rights (signal, abstract sockets) could hypothetically be extended with a related access right of another type. For instance, there could be an access right type
__u64handled_signal_number;Quoted 7 lines
and then you could add a rule to permit the use of certain signal numbers. The interaction between the scoped flags and other access rights should work the same.
Constructive Proposal for consideration: Why not both?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Quoted 7 lines
I will think about the following a bit more but I'm afraid that I feel like it might get slightly confusing. With this, the only reason for having
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETis for API consistency when we later enable allowing access to other domains (if I understood correctly), in which case I personally feel like the suggestion onlandlock_scope_attrabove, where we essentially accept that it is decoupled with the scope bits in the ruleset, might be simpler...?Mickaël expressed the opinion to me that he would like to APIs to stay consistent between signals, abstract UNIX sockets, named UNIX sockets and other future "scoped" operations, in scenarios where:
Quoted 5 lines
* the "scoped" (IPC) operations can be configured to give access to other Landlock domains (and that should work for UNIX connections too)
* the existing "scoped" operations also start having matching access rightsI think with the way I proposed, that would be consistent.
What about always implicitly set LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET (in the kernel) when LANDLOCK_ACCESS_FS_RESOLVE_UNIX is set?
*Requiring* users to set LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET would add a new way to trigger an error, but setting LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET *implicitly* looks safer. This might also help for the documentation BTW.
If we agree on this, I could merge the scope_pathname_unix in 7.0, and merge the resolve_unix in 7.1 (or later if something unexpected happen). Otherwise, I'll merge both at the same time with the same ABI version (with the risk to postpone again). WDYT?
Quoted 73 lines
Quoted 73 lines
Quoted 73 lines
Why not do both what Tingmao proposed in [1] **and** reserve the option to add the matching "scoped flag" later?
* Introduce
LANDLOCK_ACCESS_FS_RESOLVE_UNIX.If it is handled, UNIX connections are allowed either:
(1) if the connection is to a service in the same scope, or
(2) if the path was allow-listed with a "path beneath" rule.* Add
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETlater, if needed.Let's go through the arguments again:
We have observed that it is harmless to allow connections to services in the same scope (1), and that if users absolutely don't want that, they can actually prohibit it through
LANDLOCK_ACCESS_FS_MAKE_SOCK(pro1).(con1): Can we still implement the feature idea where we poke a hole
to get UNIX-connect() access to other Landlock domains?I think the answer is yes. The implementation strategy is:
* Add the scoped bit
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
* The scoped bit can now be used to allow-list connections to other Landlock domains.For users, just setting the scoped bit on its own does the same as handling
LANDLOCK_ACCESS_FS_RESOLVE_UNIX. That way, the kernel-side implementation can also stay simple. The only reason why the scoped bit is needed is because it makes it possible to allow-list connections to other Landlock domains, but at the same time, it is safe if libraries set the scoped bit once it exists, as it does not have any bad runtime impact either.(con2): Consistency: Do all the scoped flags interact with their
corresponding access rights in the same way?The other scope flags do not have corresponding access rights, so far.
If we were to add corresponding access rights for the other scope flags, I would argue that we could apply a consistent logic there, because IPC access within the same scope is always safe:
- A hypothetical access right type for "signal numbers" would only restrict signals that go beyond the current scope.
- A hypothetical access right type for "abstract UNIX domain socket names" would only restrict connections to abstract UNIX domain servers that go beyond the current scope.
I can not come up with a scenario where this doesn't work.
In conclusion, I think the approach has significant upsides:
* Simpler UAPI: Users only have one access bit to deal with, in the near future. Once we do add a scope flag for UNIX connections, it does not interact in a surprising way with the corresponding FS access right, because with either of these, scoped access is allowed.
If users absolutely need to restrict scoped access, they can restrict
LANDLOCK_ACCESS_FS_MAKE_SOCK. It is a slightly obscure API, but in line with the "make easy things easy, make hard things possible" API philosophy. And needing this should be the exception rather than the norm, after all.
Quoted 5 lines
Quoted 5 lines
Quoted 5 lines
* Consistent behaviour between scoped flags and regular access rights, also for speculative access rights affecting the existing scoped flags for signals and abstract UNIX domain sockets.
Thanks for this summary Günther!
Quoted 105 lines · Mickaël Salaün
Quoted 97 lines · Günther Noack
Quoted 84 lines · Tingmao Wang
Quoted 67 lines · Günther Noack
Quoted 46 lines
Furthermore, an application / Landlock config etc can always opt to not use the scope bit at all, if it "knows" all the locations where the application's sockets would be placed, and just use
RESOLVE_UNIXaccess right (or nothing if it is not supported).(The following is a bit of a side note, not terribly relevant if we're deciding to go with the patch as is.)
Quoted 24 lines
Quoted 14 lines
[...]
Another way to put it is that, if FS-based and scope-based controls interacts in the above proposed way, both mechanisms feel like "poking holes" in the other. But as Mickaël said, one can think of the two mechanisms not as independent controls, but rather as two interfaces for the same control. The socket access control is "enabled" if either theLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess is handled, or the scope bit proposed in this patch is enabled.With that said, I can think of some alternative ways that might make this API look "better" (from a subjective point of view, feedback welcome), however it does mean more delays, and specifically, these will depend on
LANDLOCK_ACCESS_FS_RESOLVE_UNIX:Quoted 9 lines
One possibility is to simply always allow a Landlock domain to connect to its own sockets (in the case where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled, otherwise all sockets are allowed). This might be reasonable, as one can only connect to a socket it creates if it has the permission to create it in the first place, which is already controlled byLANDLOCK_ACCESS_FS_MAKE_SOCK, so we don't really lose any policy flexibility here - if for some reason the sandboxer don't want to allow access to any (pathname) sockets, even the sandboxed app's own ones, it can just not allowLANDLOCK_ACCESS_FS_MAKE_SOCKanywhere.Quoted 3 lines
LANDLOCK_ACCESS_FS_MAKE_SOCKis only required to bind/listen to a socket, not to connect. I guess you was thinking aboutLANDLOCK_ACCESS_FS_RESOLVE_UNIXin this case?In this "allow same-scope connect unconditionally" proposal, the application would still be able to (bind to and) connect to its own sockets, even if
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled and nothing is allowed to haveLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess. But a sandboxer which for whatever reason doesn't want this "allow same scope" default can still prevent the use of (pathname) sockets by restrictingLANDLOCK_ACCESS_FS_MAKE_SOCK, because if an app can't connect to any sockets it doesn't own, and can't create any sockets itself either, then it effectively can't connect to any sockets at all.Quoted 4 lines
(Although on second thought, I guess there could be a case where an app first creates some socket files before doing
landlock_restrict_self(), then it might still be able to bind to these even withoutLANDLOCK_ACCESS_FS_MAKE_SOCK?)FWIW, I also really liked Tingmao's first of the two listed possibilities in [1], where she proposed to introduce both rights together. In my understanding, the arguments we have discussed so far for that are:
IN FAVOR:
(pro1) Connecting to a UNIX socket in the same scope is always safe,
and it makes it possible to use named UNIX sockets between the processes within a Landlock domains. (Mickaël convinced me in discussion at FOSDEM that this is true.)If someone absolutely does not want that, they can restrict
LANDLOCK_ACCESS_FS_MAKE_SOCKand achieve the same effect (as Tingmao said above).Quoted 5 lines
(pro2) The implementation of this is simpler.
(I attempted to understand how the "or" semantics would be implemented, and I found it non-trivial when you try to do it for all layers at once. (Kernighan's Law applies, IMHO))
I think the logic would basically be:
1. if any layers deny the access due to handled
RESOLVE_UNIXbut does not have the scope bit set, then we will deny rightaway, without callingdomain_is_scoped().2. Call
domain_is_scoped()with a bitmask of "rules_covered" layers where there areRESOLVE_UNIXrules covering the socket being accessed, and essentially ignore those layers in the scope violation check.Quoted 6 lines
I definitely agree that it is tricky, but making same-scope access be allowed (i.e. the suggested idea above) would only get rid of step 1, which I think is the "simpler" bit. The extra logic in step 2 is still needed.
I definitely agree with pro1 tho.
Yes, you are describing the logic for the variant where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXdoes not implicitly permit access from within the same scope. In that variant, there can be situations where the first hook can deny the action immediately.Quoted 6 lines
In the variant where
LANDLOCK_ACCESS_FS_RESOLVE_UNIX*does* implicitly allow access from within the same scope, that shortcutting is not possible. On the upside however, there is no need to distinguish whether the scope flag is set when we are in thesecurity_unix_find()hook, because access from within the same scope is always permitted. (That is the simplification I meant.)
Quoted 6 lines
This proposal make sense, improve IPC restriction consistency (by always be able to connect to peers from the same domain), and would simplify some checks.
What bothers me the most is the implicit scoping induced by the
FS_RESOLVE_UNIXhandling.
I see this as similar to other decisions we have taken, e.g. on the (in-review) socket type patch, we have also discussed at the time that socketpair() should always be allowed, even though it creates sockets, because this syscall only establishes IPC communication channels within the same Landlock domain.
Admittedly, in this case it is a bit more clearly about the Landlock domain boundaries, but still OK in my mind. I think we are aligned though, see my answer to your proposal below.
Quoted 4 lines
A related issue is the mix of checks
for theFS_RESOLVE_UNIXimplementation, which could make the code more complex, but I don't see a better way. Landlock already has an implicit ptrace scoping but it's not (and should never be) optional.
I am afraid that we don't have a better option. Even if we only introduced one of the two patch sets in its original form now, we would have to introduce that implementation as soon as the other is added.
Quoted 75 lines
See my proposal below about implicit
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET. That would not change the kernel checks you described though.Quoted 50 lines
Quoted 25 lines
Quoted 14 lines
AGAINST:
(con1) It would work differently than the other scoped access rights
that we already have.A speculative feature that could potentially be built with the scoped access rights is that we could add a rule to permit IPC to other Landlock scopes, e.g. introducing a new rule type
struct
landlock_scope_attr{
__u64allowed_access; /* for "scoped" bits */ /* some way to identify domains */
}Quoted 6 lines
so that we could make IPC access to other Landlock domains configurable.
If the scoped bit and the FS
RESOLVE_UNIXbit were both conflated inRESOLVE_UNIX, it would not be possible to make UNIX connections configurable in such a way.This exact API would no longer work, but if we give up the equivalence between scope bits and the
landlock_scope_attrstruct, then we can do something like:Quoted 13 lines
struct landlock_scope_attr { __u64 ptrace:1; /* Note that this is not a (user controllable) scope bit! */ __u64 abstract_unix_socket:1; __u64 pathname_unix_socket:1; /* ... */ __u64 allowed_signals; /* * some way to identify domains, maybe we could use the audit domain * ID, with 0 denoting "allow access to non-Landlocked processes? */ }Yes, it would be possible to use such a struct for that scenario where IPC access gets allowed for other Landlock scopes. It would mean that we would not need to introduce a scoped flag for the pathname UNIX socket connections. But the relationship between that struct
landlock_scope_attrand the flags and access rights in structlandlock_ruleset_attrwould become less clear, which is a slight downside, and maybe error prone for users to work with.If we introduced an additional scoped flag, it would also be consistent though.
Quoted 8 lines
(con1) was written under the assumption that we do not have an additional scoped flag. If that is lacking, it is not possible to express UNIX
connect()access to other Landlock domains with that struct. But as outlined in the proposal below, if we *do* (later) introduce the additional scoped flag *in addition* to the FS access right, this *both* stays consistent in semantics with the signal and abstract UNIX support, *and* it starts working in a world where ICP access can be allowed to talk to other Landlock domains.In this case, we need to assume the scoped flag is already there, and because we have the implementation, to actually add it now.
As a side note, I don't really like this
landlock_scope_attrinterface. A dedicated attr per kind of IPC would be much more flexible (e.g. thehandled_signal_numberyou described below). For instance, even if at first we don't make it possible to describe different signals within the attr struct, this struct will be able to grow. So it's better to have a dedicated interface per IPC type than a generic scope interface that can only express a subset of it.
Quoted 2 lines
Anyway, this proposal could work for pathname UNIX sockets, but a future signal attr would be a bit inconsistent wrt to the pathname UNIX one.
See my answer to your proposal below.
Quoted 43 lines
Quoted 29 lines
Quoted 17 lines
Quoted 9 lines
(con2) Consistent behaviour between scoped flags and their
interactions with other access rights:The existing scoped access rights (signal, abstract sockets) could hypothetically be extended with a related access right of another type. For instance, there could be an access right type
__u64handled_signal_number;Quoted 7 lines
and then you could add a rule to permit the use of certain signal numbers. The interaction between the scoped flags and other access rights should work the same.
Constructive Proposal for consideration: Why not both?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Quoted 7 lines
I will think about the following a bit more but I'm afraid that I feel like it might get slightly confusing. With this, the only reason for having
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETis for API consistency when we later enable allowing access to other domains (if I understood correctly), in which case I personally feel like the suggestion onlandlock_scope_attrabove, where we essentially accept that it is decoupled with the scope bits in the ruleset, might be simpler...?Mickaël expressed the opinion to me that he would like to APIs to stay consistent between signals, abstract UNIX sockets, named UNIX sockets and other future "scoped" operations, in scenarios where:
Quoted 5 lines
* the "scoped" (IPC) operations can be configured to give access to other Landlock domains (and that should work for UNIX connections too)
* the existing "scoped" operations also start having matching access rightsI think with the way I proposed, that would be consistent.
What about always implicitly set
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET(in the kernel) whenLANDLOCK_ACCESS_FS_RESOLVE_UNIXis set?*Requiring* users to set
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETwould add a new way to trigger an error, but settingLANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET*implicitly* looks safer. This might also help for the documentation BTW.
Quoted 4 lines
If we agree on this, I could merge the
scope_pathname_unixin 7.0, and merge theresolve_unixin 7.1 (or later if something unexpected happen). Otherwise, I'll merge both at the same time with the same ABI version (with the risk to postpone again). WDYT?
In my understanding, the only thing in which our two proposals differ is the order in which we introduce the LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET and LANDLOCK_ACCESS_FS_RESOLVE_UNIX bits to the UAPI.
As soon as both bits are added, our two proposals do the same thing.
My proposal (introduce LANDLOCK_ACCESS_FS_RESOLVE_UNIX first)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The reason why I have been arguing to introduceLANDLOCK_ACCESS_FS_RESOLVE_UNIX first is:
(a) It is possible (which initially wasn't obvious to me):
When implementing this, handling LANDLOCK_ACCESS_FS_RESOLVE_UNIX implies the LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET semantics(!), even when that bit is not exposed in the UAPI yet. (When LANDLOCK_ACCESS_FS_RESOLVE_UNIX is handled, UNIX-connect always works within the same Landlock domain, even for the time when the scoped flag is not exposed in the UAPI yet.)
That way, everything stays compatible even when the scoped flag is later introduced.
(b) When introducing LANDLOCK_ACCESS_FS_RESOLVE_UNIX first, the UAPI will initially only expose one of the two bits.
This simplifies the UAPI because users initially do not need to deal with the fact that two bits in the UAPI "interact".
We can still introduce LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET later. But the only ruleset-building feature for which this needed would be if we want to have rules that allow outwards IPC interactions with other Landlock domains.
Possibility of YAGNI: I am not sure how certain you are that the speculative allowing-scope-access feature is needed; I mentioned it here only because it is an option we want to keep open. If we end up *not* needing this feature though, we will *not need to introduce the LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET bit at all*, because LANDLOCK_ACCESS_FS_RESOLVE_UNIX can already do all that is needed.
The implementation of this approach would be that we would have to join the functionality from the scoped and FS-based patch set, but without introducing the LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET flag in the UAPI.
Your proposal (introduce LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET first)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Upside: users won't be surprised if UNIX-connect within the same Landlock domain works, because the bit is there for it
* Downside: They'll have to understand that the FS bit implies the scope flag.
But as I said in the beginning -- if we add both flags, these two proposals end up being the same in the end.
Maybe the main point that is unclear to me is what I called the "Possibility of YAGNI" above: it is at this point not 100% clear to me that the feature for allowing IPC to other domains is going to happen (we don't have a bug tracker issue for it either, AFAICT). So in the case where this does not actually end up happening, I think that my approach has the advantage of introducing one flag less, so the UAPI stays simpler.
If you know that that feature will happen though, I think the two approaches are equally good in the long run.
But either way, Mickaël, you are the maintainer here :); if you prefer to do the LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET patch set in its current form first, it's also OK with me. As I said, I think these two approaches are the same.
P.S. Tingmao: One additional complication, in case the LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET patch set gets sent first -- your current patch set uses a different LSM hook than the final state which we are aiming for. We may need to double check that this will stay compatible even if we move the scoped-check into the new LSM hook in the future. (If we move the check into a different hook, there may be some obscure scenarios in which a denied connect()/send() results in a different error code.) I may comment on the patch set.
Quoted 78 lines
Quoted 71 lines
Quoted 71 lines
Quoted 71 lines
Why not do both what Tingmao proposed in [1] **and** reserve the option to add the matching "scoped flag" later?
* Introduce
LANDLOCK_ACCESS_FS_RESOLVE_UNIX.If it is handled, UNIX connections are allowed either:
(1) if the connection is to a service in the same scope, or
(2) if the path was allow-listed with a "path beneath" rule.* Add
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETlater, if needed.Let's go through the arguments again:
We have observed that it is harmless to allow connections to services in the same scope (1), and that if users absolutely don't want that, they can actually prohibit it through
LANDLOCK_ACCESS_FS_MAKE_SOCK(pro1).(con1): Can we still implement the feature idea where we poke a hole
to get UNIX-connect() access to other Landlock domains?I think the answer is yes. The implementation strategy is:
* Add the scoped bit
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
* The scoped bit can now be used to allow-list connections to other Landlock domains.For users, just setting the scoped bit on its own does the same as handling
LANDLOCK_ACCESS_FS_RESOLVE_UNIX. That way, the kernel-side implementation can also stay simple. The only reason why the scoped bit is needed is because it makes it possible to allow-list connections to other Landlock domains, but at the same time, it is safe if libraries set the scoped bit once it exists, as it does not have any bad runtime impact either.(con2): Consistency: Do all the scoped flags interact with their
corresponding access rights in the same way?The other scope flags do not have corresponding access rights, so far.
If we were to add corresponding access rights for the other scope flags, I would argue that we could apply a consistent logic there, because IPC access within the same scope is always safe:
- A hypothetical access right type for "signal numbers" would only restrict signals that go beyond the current scope.
- A hypothetical access right type for "abstract UNIX domain socket names" would only restrict connections to abstract UNIX domain servers that go beyond the current scope.
I can not come up with a scenario where this doesn't work.
In conclusion, I think the approach has significant upsides:
* Simpler UAPI: Users only have one access bit to deal with, in the near future. Once we do add a scope flag for UNIX connections, it does not interact in a surprising way with the corresponding FS access right, because with either of these, scoped access is allowed.
If users absolutely need to restrict scoped access, they can restrict
LANDLOCK_ACCESS_FS_MAKE_SOCK. It is a slightly obscure API, but in line with the "make easy things easy, make hard things possible" API philosophy. And needing this should be the exception rather than the norm, after all.Quoted 5 lines
Quoted 5 lines
Quoted 5 lines
* Consistent behaviour between scoped flags and regular access rights, also for speculative access rights affecting the existing scoped flags for signals and abstract UNIX domain sockets.
Thanks for this summary Günther!
Mickaël Salaün
maintainerQuoted 286 lines · Günther Noack
Quoted 105 lines · Mickaël Salaün
Quoted 97 lines · Günther Noack
Quoted 84 lines · Tingmao Wang
Quoted 67 lines · Günther Noack
Quoted 46 lines
Furthermore, an application / Landlock config etc can always opt to not use the scope bit at all, if it "knows" all the locations where the application's sockets would be placed, and just use
RESOLVE_UNIXaccess right (or nothing if it is not supported).(The following is a bit of a side note, not terribly relevant if we're deciding to go with the patch as is.)
Quoted 24 lines
Quoted 14 lines
[...]
Another way to put it is that, if FS-based and scope-based controls interacts in the above proposed way, both mechanisms feel like "poking holes" in the other. But as Mickaël said, one can think of the two mechanisms not as independent controls, but rather as two interfaces for the same control. The socket access control is "enabled" if either theLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess is handled, or the scope bit proposed in this patch is enabled.With that said, I can think of some alternative ways that might make this API look "better" (from a subjective point of view, feedback welcome), however it does mean more delays, and specifically, these will depend on
LANDLOCK_ACCESS_FS_RESOLVE_UNIX:Quoted 9 lines
One possibility is to simply always allow a Landlock domain to connect to its own sockets (in the case where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled, otherwise all sockets are allowed). This might be reasonable, as one can only connect to a socket it creates if it has the permission to create it in the first place, which is already controlled byLANDLOCK_ACCESS_FS_MAKE_SOCK, so we don't really lose any policy flexibility here - if for some reason the sandboxer don't want to allow access to any (pathname) sockets, even the sandboxed app's own ones, it can just not allowLANDLOCK_ACCESS_FS_MAKE_SOCKanywhere.Quoted 3 lines
LANDLOCK_ACCESS_FS_MAKE_SOCKis only required to bind/listen to a socket, not to connect. I guess you was thinking aboutLANDLOCK_ACCESS_FS_RESOLVE_UNIXin this case?In this "allow same-scope connect unconditionally" proposal, the application would still be able to (bind to and) connect to its own sockets, even if
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled and nothing is allowed to haveLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess. But a sandboxer which for whatever reason doesn't want this "allow same scope" default can still prevent the use of (pathname) sockets by restrictingLANDLOCK_ACCESS_FS_MAKE_SOCK, because if an app can't connect to any sockets it doesn't own, and can't create any sockets itself either, then it effectively can't connect to any sockets at all.Quoted 4 lines
(Although on second thought, I guess there could be a case where an app first creates some socket files before doing
landlock_restrict_self(), then it might still be able to bind to these even withoutLANDLOCK_ACCESS_FS_MAKE_SOCK?)FWIW, I also really liked Tingmao's first of the two listed possibilities in [1], where she proposed to introduce both rights together. In my understanding, the arguments we have discussed so far for that are:
IN FAVOR:
(pro1) Connecting to a UNIX socket in the same scope is always safe,
and it makes it possible to use named UNIX sockets between the processes within a Landlock domains. (Mickaël convinced me in discussion at FOSDEM that this is true.)If someone absolutely does not want that, they can restrict
LANDLOCK_ACCESS_FS_MAKE_SOCKand achieve the same effect (as Tingmao said above).Quoted 5 lines
(pro2) The implementation of this is simpler.
(I attempted to understand how the "or" semantics would be implemented, and I found it non-trivial when you try to do it for all layers at once. (Kernighan's Law applies, IMHO))
I think the logic would basically be:
1. if any layers deny the access due to handled
RESOLVE_UNIXbut does not have the scope bit set, then we will deny rightaway, without callingdomain_is_scoped().2. Call
domain_is_scoped()with a bitmask of "rules_covered" layers where there areRESOLVE_UNIXrules covering the socket being accessed, and essentially ignore those layers in the scope violation check.Quoted 6 lines
I definitely agree that it is tricky, but making same-scope access be allowed (i.e. the suggested idea above) would only get rid of step 1, which I think is the "simpler" bit. The extra logic in step 2 is still needed.
I definitely agree with pro1 tho.
Yes, you are describing the logic for the variant where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXdoes not implicitly permit access from within the same scope. In that variant, there can be situations where the first hook can deny the action immediately.Quoted 6 lines
In the variant where
LANDLOCK_ACCESS_FS_RESOLVE_UNIX*does* implicitly allow access from within the same scope, that shortcutting is not possible. On the upside however, there is no need to distinguish whether the scope flag is set when we are in thesecurity_unix_find()hook, because access from within the same scope is always permitted. (That is the simplification I meant.)Quoted 6 lines
This proposal make sense, improve IPC restriction consistency (by always be able to connect to peers from the same domain), and would simplify some checks.
What bothers me the most is the implicit scoping induced by the
FS_RESOLVE_UNIXhandling.I see this as similar to other decisions we have taken, e.g. on the (in-review) socket type patch, we have also discussed at the time that
socketpair()should always be allowed, even though it creates sockets, because this syscall only establishes IPC communication channels within the same Landlock domain.Admittedly, in this case it is a bit more clearly about the Landlock domain boundaries, but still OK in my mind. I think we are aligned though, see my answer to your proposal below.
Quoted 4 lines
A related issue is the mix of checks
for theFS_RESOLVE_UNIXimplementation, which could make the code more complex, but I don't see a better way. Landlock already has an implicit ptrace scoping but it's not (and should never be) optional.I am afraid that we don't have a better option. Even if we only introduced one of the two patch sets in its original form now, we would have to introduce that implementation as soon as the other is added.
Quoted 75 lines
See my proposal below about implicit
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET. That would not change the kernel checks you described though.Quoted 50 lines
Quoted 25 lines
Quoted 14 lines
AGAINST:
(con1) It would work differently than the other scoped access rights
that we already have.A speculative feature that could potentially be built with the scoped access rights is that we could add a rule to permit IPC to other Landlock scopes, e.g. introducing a new rule type
struct
landlock_scope_attr{
__u64allowed_access; /* for "scoped" bits */ /* some way to identify domains */
}Quoted 6 lines
so that we could make IPC access to other Landlock domains configurable.
If the scoped bit and the FS
RESOLVE_UNIXbit were both conflated inRESOLVE_UNIX, it would not be possible to make UNIX connections configurable in such a way.This exact API would no longer work, but if we give up the equivalence between scope bits and the
landlock_scope_attrstruct, then we can do something like:Quoted 13 lines
struct landlock_scope_attr { __u64 ptrace:1; /* Note that this is not a (user controllable) scope bit! */ __u64 abstract_unix_socket:1; __u64 pathname_unix_socket:1; /* ... */ __u64 allowed_signals; /* * some way to identify domains, maybe we could use the audit domain * ID, with 0 denoting "allow access to non-Landlocked processes? */ }Yes, it would be possible to use such a struct for that scenario where IPC access gets allowed for other Landlock scopes. It would mean that we would not need to introduce a scoped flag for the pathname UNIX socket connections. But the relationship between that struct
landlock_scope_attrand the flags and access rights in structlandlock_ruleset_attrwould become less clear, which is a slight downside, and maybe error prone for users to work with.If we introduced an additional scoped flag, it would also be consistent though.
Quoted 8 lines
(con1) was written under the assumption that we do not have an additional scoped flag. If that is lacking, it is not possible to express UNIX
connect()access to other Landlock domains with that struct. But as outlined in the proposal below, if we *do* (later) introduce the additional scoped flag *in addition* to the FS access right, this *both* stays consistent in semantics with the signal and abstract UNIX support, *and* it starts working in a world where ICP access can be allowed to talk to other Landlock domains.In this case, we need to assume the scoped flag is already there, and because we have the implementation, to actually add it now.
As a side note, I don't really like this
landlock_scope_attrinterface. A dedicated attr per kind of IPC would be much more flexible (e.g. thehandled_signal_numberyou described below). For instance, even if at first we don't make it possible to describe different signals within the attr struct, this struct will be able to grow. So it's better to have a dedicated interface per IPC type than a generic scope interface that can only express a subset of it.Quoted 2 lines
Anyway, this proposal could work for pathname UNIX sockets, but a future signal attr would be a bit inconsistent wrt to the pathname UNIX one.
See my answer to your proposal below.
Quoted 43 lines
Quoted 29 lines
Quoted 17 lines
Quoted 9 lines
(con2) Consistent behaviour between scoped flags and their
interactions with other access rights:The existing scoped access rights (signal, abstract sockets) could hypothetically be extended with a related access right of another type. For instance, there could be an access right type
__u64handled_signal_number;Quoted 7 lines
and then you could add a rule to permit the use of certain signal numbers. The interaction between the scoped flags and other access rights should work the same.
Constructive Proposal for consideration: Why not both?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Quoted 7 lines
I will think about the following a bit more but I'm afraid that I feel like it might get slightly confusing. With this, the only reason for having
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETis for API consistency when we later enable allowing access to other domains (if I understood correctly), in which case I personally feel like the suggestion onlandlock_scope_attrabove, where we essentially accept that it is decoupled with the scope bits in the ruleset, might be simpler...?Mickaël expressed the opinion to me that he would like to APIs to stay consistent between signals, abstract UNIX sockets, named UNIX sockets and other future "scoped" operations, in scenarios where:
Quoted 5 lines
* the "scoped" (IPC) operations can be configured to give access to other Landlock domains (and that should work for UNIX connections too)
* the existing "scoped" operations also start having matching access rightsI think with the way I proposed, that would be consistent.
What about always implicitly set
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET(in the kernel) whenLANDLOCK_ACCESS_FS_RESOLVE_UNIXis set?*Requiring* users to set
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETwould add a new way to trigger an error, but settingLANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET*implicitly* looks safer. This might also help for the documentation BTW.Quoted 4 lines
If we agree on this, I could merge the
scope_pathname_unixin 7.0, and merge theresolve_unixin 7.1 (or later if something unexpected happen). Otherwise, I'll merge both at the same time with the same ABI version (with the risk to postpone again). WDYT?In my understanding, the only thing in which our two proposals differ is the order in which we introduce the
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETandLANDLOCK_ACCESS_FS_RESOLVE_UNIXbits to the UAPI.As soon as both bits are added, our two proposals do the same thing.
My proposal (introduce
LANDLOCK_ACCESS_FS_RESOLVE_UNIXfirst)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~The reason why I have been arguing to introduce
LANDLOCK_ACCESS_FS_RESOLVE_UNIXfirst is:(a) It is possible (which initially wasn't obvious to me):
When implementing this, handling
LANDLOCK_ACCESS_FS_RESOLVE_UNIXimplies theLANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETsemantics(!), even when that bit is not exposed in the UAPI yet. (WhenLANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled, UNIX-connect always works within the same Landlock domain, even for the time when the scoped flag is not exposed in the UAPI yet.)
Quoted 8 lines
That way, everything stays compatible even when the scoped flag is later introduced.
(b) When introducing
LANDLOCK_ACCESS_FS_RESOLVE_UNIXfirst, the UAPI will initially only expose one of the two bits.This simplifies the UAPI because users initially do not need to deal with the fact that two bits in the UAPI "interact".
It simplifies but users can just ignore the bits for which they don't care. They just have to set the one they want (either scope or resolve) and Landlock will do the right thing. :)
The only corner case is that setting scope_pathname_unix or not when resolve_unix is set will result to the same restrictions. In other words, resolve_unix is a superset of scope_pathname_unix, so this scope UAPI bit will be useless when resolve_unix will be introduced.
Quoted 6 lines
We can still introduce
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETlater. But the only ruleset-building feature for which this needed would be if we want to have rules that allow outwards IPC interactions with other Landlock domains.
Quoted 7 lines
Possibility of YAGNI: I am not sure how certain you are that the speculative allowing-scope-access feature is needed; I mentioned it here only because it is an option we want to keep open. If we end up *not* needing this feature though, we will *not need to introduce the
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETbit at all*, becauseLANDLOCK_ACCESS_FS_RESOLVE_UNIXcan already do all that is needed.
Indeed, I get your point. Please add this rationale in the "Design choices" section in Documentation/security/landlock.rst
Quoted 5 lines
The implementation of this approach would be that we would have to join the functionality from the scoped and FS-based patch set, but without introducing the
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETflag in the UAPI.
Right, this looks good to me. We'll need to sync both patch series and remove the scope flag from UAPI. I'll let you and Tingmao work together for the next series. The "IPC scoping" documentation section should mention LANDLOCK_ACCESS_FS_RESOLVE_UNIX even if it's not a scope flag.
Quoted 10 lines
Your proposal (introduce
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETfirst)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~* Upside: users won't be surprised if UNIX-connect within the same Landlock domain works, because the bit is there for it
* Downside: They'll have to understand that the FS bit implies the scope flag.
Yes, but this would be the same with your approach. Not sure which one would be the less confusing though.
Quoted 6 lines
But as I said in the beginning -- if we add both flags, these two proposals end up being the same in the end.
Quoted 7 lines
Maybe the main point that is unclear to me is what I called the "Possibility of YAGNI" above: it is at this point not 100% clear to me that the feature for allowing IPC to other domains is going to happen (we don't have a bug tracker issue for it either, AFAICT). So in the case where this does not actually end up happening, I think that my approach has the advantage of introducing one flag less, so the UAPI stays simpler.
As I said earlier, I'd definitely prefer to have an IPC-specific attr instead of a generic scope attr. So yes, this looks good to me.
Quoted 8 lines
If you know that that feature will happen though, I think the two approaches are equally good in the long run.
But either way, Mickaël, you are the maintainer here :); if you prefer to do the
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETpatch set in its current form first, it's also OK with me. As I said, I think these two approaches are the same.
You convinced me. Let's go for the hybrid approach you proposed with the implicit scope.
Quoted 10 lines
P.S. Tingmao: One additional complication, in case theLANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETpatch set gets sent first -- your current patch set uses a different LSM hook than the final state which we are aiming for. We may need to double check that this will stay compatible even if we move the scoped-check into the new LSM hook in the future. (If we move the check into a different hook, there may be some obscure scenarios in which a denied connect()/send() results in a different error code.) I may comment on the patch set.
That's also a good point. I'd be curious to know if this changes anything, but it should not except maybe some error codes.
Anyway, we'll need this new LSM hook.
Quoted 80 lines
Quoted 79 lines
Quoted 72 lines
Quoted 72 lines
Quoted 72 lines
Why not do both what Tingmao proposed in [1] **and** reserve the option to add the matching "scoped flag" later?
* Introduce
LANDLOCK_ACCESS_FS_RESOLVE_UNIX.If it is handled, UNIX connections are allowed either:
(1) if the connection is to a service in the same scope, or
(2) if the path was allow-listed with a "path beneath" rule.* Add
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETlater, if needed.Let's go through the arguments again:
We have observed that it is harmless to allow connections to services in the same scope (1), and that if users absolutely don't want that, they can actually prohibit it through
LANDLOCK_ACCESS_FS_MAKE_SOCK(pro1).(con1): Can we still implement the feature idea where we poke a hole
to get UNIX-connect() access to other Landlock domains?I think the answer is yes. The implementation strategy is:
* Add the scoped bit
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
* The scoped bit can now be used to allow-list connections to other Landlock domains.For users, just setting the scoped bit on its own does the same as handling
LANDLOCK_ACCESS_FS_RESOLVE_UNIX. That way, the kernel-side implementation can also stay simple. The only reason why the scoped bit is needed is because it makes it possible to allow-list connections to other Landlock domains, but at the same time, it is safe if libraries set the scoped bit once it exists, as it does not have any bad runtime impact either.(con2): Consistency: Do all the scoped flags interact with their
corresponding access rights in the same way?The other scope flags do not have corresponding access rights, so far.
If we were to add corresponding access rights for the other scope flags, I would argue that we could apply a consistent logic there, because IPC access within the same scope is always safe:
- A hypothetical access right type for "signal numbers" would only restrict signals that go beyond the current scope.
- A hypothetical access right type for "abstract UNIX domain socket names" would only restrict connections to abstract UNIX domain servers that go beyond the current scope.
I can not come up with a scenario where this doesn't work.
In conclusion, I think the approach has significant upsides:
* Simpler UAPI: Users only have one access bit to deal with, in the near future. Once we do add a scope flag for UNIX connections, it does not interact in a surprising way with the corresponding FS access right, because with either of these, scoped access is allowed.
If users absolutely need to restrict scoped access, they can restrict
LANDLOCK_ACCESS_FS_MAKE_SOCK. It is a slightly obscure API, but in line with the "make easy things easy, make hard things possible" API philosophy. And needing this should be the exception rather than the norm, after all.Quoted 5 lines
Quoted 5 lines
Quoted 5 lines
* Consistent behaviour between scoped flags and regular access rights, also for speculative access rights affecting the existing scoped flags for signals and abstract UNIX domain sockets.
Thanks for this summary Günther!
Quoted 8 lines · Mickaël Salaün
Quoted 6 lines · Günther Noack
[...]
The implementation of this approach would be that we would have to join the functionality from the scoped and FS-based patch set, but without introducing the
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETflag in the UAPI.
Quoted 4 lines
Right, this looks good to me. We'll need to sync both patch series and remove the scope flag from UAPI. I'll let you and Tingmao work together for the next series. The "IPC scoping" documentation section should mention
LANDLOCK_ACCESS_FS_RESOLVE_UNIXeven if it's not a scope flag.
This sounds good to me. I'm not sure how much code we can reuse out of the existing LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET patchset - but I think the selftest patches could still largely be useful (after changing e.g. create_scoped_domain() to use the RESOLVE_UNIX fs access instead of the scope bit for pathname sockets). The fs-based rules (i.e. "exceptions") can then be tested separately from the scope tests (and would also check for things like path being different across mount namespaces etc).
Günther, feel free to take anything out of the existing scope series, if you feel it would be useful. Also let me know if you would like me to help with any part of the RESOLVE_UNIX series if you feel that would be useful (but you don't have to if not).
Quoted 22 lines · Tingmao Wang
Quoted 8 lines · Mickaël Salaün
Quoted 6 lines · Günther Noack
[...]
The implementation of this approach would be that we would have to join the functionality from the scoped and FS-based patch set, but without introducing the
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETflag in the UAPI.Quoted 4 lines
Right, this looks good to me. We'll need to sync both patch series and remove the scope flag from UAPI. I'll let you and Tingmao work together for the next series. The "IPC scoping" documentation section should mention
LANDLOCK_ACCESS_FS_RESOLVE_UNIXeven if it's not a scope flag.This sounds good to me. I'm not sure how much code we can reuse out of the existing
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETpatchset - but I think the selftest patches could still largely be useful (after changing e.g.create_scoped_domain()to use theRESOLVE_UNIXfs access instead of the scope bit for pathname sockets). The fs-based rules (i.e. "exceptions") can then be tested separately from the scope tests (and would also check for things like path being different across mount namespaces etc).
Quoted 4 lines
Günther, feel free to take anything out of the existing scope series, if you feel it would be useful. Also let me know if you would like me to help with any part of the
RESOLVE_UNIXseries if you feel that would be useful (but you don't have to if not).
Thank you, Tingmao!
So far, the selftests that I already had in fs_test.c were straightforward to extend so that they cover the new cases. I had a look at your patch set, but found the scoping tests difficult to port to fs_test.c, but I'll double check that we don't miss anything. Either way, I'll make sure that you'll get appropriate credit for it. :)
–Günther
(P.S. If this mail looks familiar, it's because I accidentally replied with an earlier version of that to the wrong mail earlier today
(https://lore.kernel.org/all/[email protected]/) – Replying here again so that this answer makes more sense.)
Quoted 28 lines · Günther Noack
Quoted 22 lines · Tingmao Wang
Quoted 8 lines · Mickaël Salaün
Quoted 6 lines · Günther Noack
[...]
The implementation of this approach would be that we would have to join the functionality from the scoped and FS-based patch set, but without introducing the
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETflag in the UAPI.Quoted 4 lines
Right, this looks good to me. We'll need to sync both patch series and remove the scope flag from UAPI. I'll let you and Tingmao work together for the next series. The "IPC scoping" documentation section should mention
LANDLOCK_ACCESS_FS_RESOLVE_UNIXeven if it's not a scope flag.This sounds good to me. I'm not sure how much code we can reuse out of the existing
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETpatchset - but I think the selftest patches could still largely be useful (after changing e.g.create_scoped_domain()to use theRESOLVE_UNIXfs access instead of the scope bit for pathname sockets). The fs-based rules (i.e. "exceptions") can then be tested separately from the scope tests (and would also check for things like path being different across mount namespaces etc).Quoted 4 lines
Günther, feel free to take anything out of the existing scope series, if you feel it would be useful. Also let me know if you would like me to help with any part of the
RESOLVE_UNIXseries if you feel that would be useful (but you don't have to if not).
Quoted 6 lines
Thank you, Tingmao!
So far, the selftests that I already had in
fs_test.cwere straightforward to extend so that they cover the new cases. I had a look at your patch set, but found the scoping tests difficult to port tofs_test.c
I was thinking that the tests in scoped_abstract_unix_test.c could be extended to test scoping of pathname UNIX sockets as well (otherwise wouldn't you have to write another instance of the scoped_domains test based on scoped_base_variants.h, whether you put it in fs_test.c or somewhere else?)
And if you think that is sensible, then I'm hoping that patch 4,5,6 of the series would be mostly useful. But it's up to you :)
Quoted 3 lines
, but I'll double check that we don't miss anything. Either way, I'll make sure that you'll get appropriate credit for it. :)
Thanks!
Tingmao
Quoted 1 line
...
Quoted 8 lines · Tingmao Wang
Quoted 6 lines · Günther Noack
Thank you, Tingmao!
So far, the selftests that I already had in
fs_test.cwere straightforward to extend so that they cover the new cases. I had a look at your patch set, but found the scoping tests difficult to port tofs_test.c
Quoted 8 lines
I was thinking that the tests in
scoped_abstract_unix_test.ccould be extended to test scoping of pathname UNIX sockets as well (otherwise wouldn't you have to write another instance of thescoped_domainstest based onscoped_base_variants.h, whether you put it infs_test.cor somewhere else?)And if you think that is sensible, then I'm hoping that patch 4,5,6 of the series would be mostly useful. But it's up to you :)
I maybe have not wrapped my head around the scoped_test enough; for now I sent a tentative V4 patch set to the list, so that we can discuss something concrete.
If you spot things that are missing, or you feel inspired to port your tests on top, I am still happy to accept that. (But for today it is too late in the evening here %-))
–Günther
Mickaël Salaün
maintainerQuoted 36 lines · Tingmao Wang
Quoted 28 lines · Günther Noack
Quoted 22 lines · Tingmao Wang
Quoted 8 lines · Mickaël Salaün
Quoted 6 lines · Günther Noack
[...]
The implementation of this approach would be that we would have to join the functionality from the scoped and FS-based patch set, but without introducing the
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETflag in the UAPI.Quoted 4 lines
Right, this looks good to me. We'll need to sync both patch series and remove the scope flag from UAPI. I'll let you and Tingmao work together for the next series. The "IPC scoping" documentation section should mention
LANDLOCK_ACCESS_FS_RESOLVE_UNIXeven if it's not a scope flag.This sounds good to me. I'm not sure how much code we can reuse out of the existing
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKETpatchset - but I think the selftest patches could still largely be useful (after changing e.g.create_scoped_domain()to use theRESOLVE_UNIXfs access instead of the scope bit for pathname sockets). The fs-based rules (i.e. "exceptions") can then be tested separately from the scope tests (and would also check for things like path being different across mount namespaces etc).Quoted 4 lines
Günther, feel free to take anything out of the existing scope series, if you feel it would be useful. Also let me know if you would like me to help with any part of the
RESOLVE_UNIXseries if you feel that would be useful (but you don't have to if not).Quoted 6 lines
Thank you, Tingmao!
So far, the selftests that I already had in
fs_test.cwere straightforward to extend so that they cover the new cases. I had a look at your patch set, but found the scoping tests difficult to port tofs_test.c
Quoted 8 lines
I was thinking that the tests in
scoped_abstract_unix_test.ccould be extended to test scoping of pathname UNIX sockets as well (otherwise wouldn't you have to write another instance of thescoped_domainstest based onscoped_base_variants.h, whether you put it infs_test.cor somewhere else?)And if you think that is sensible, then I'm hoping that patch 4,5,6 of the series would be mostly useful. But it's up to you :)
I agree that these 3 patches should be integrated (see my other reply on Günther's v4).
Quoted 11 lines
Quoted 4 lines
, but I'll double check that we don't miss anything. Either way, I'll make sure that you'll get appropriate credit for it. :)
Thanks!
Tingmao
Quoted 1 line
...
Mickaël Salaün
maintainerQuoted 59 lines · Tingmao Wang
Hi Mickaël,
Thanks for the feedback and explanations :)
Quoted 14 lines · Mickaël Salaün
Quoted 2 lines · Tingmao Wang
[...]
What do folks think?I'd like to keep a clean API, with a "scoped" field handling IPC scoping, and an "
handled_access_fs" field handling filesystem-related accesses.One thing to keep in mind is that we could add a new kind of "handled" field that would enable to add rules identifying e.g. processes, cgroups, or Landlock domains, and that could be used to add exceptions to the current scopes. This means that we need to have a generic way to handle this case.
Quoted 7 lines
What is the issue with two complementary interfaces (scope and access) used to express a policy about connecting to UNIX sockets? We just need to make sure that scopes and
handled_access_fsdealing with UNIX sockets are like binary OR: if the scope is set, then the domain can communicate with peers which are in the same domain, and if thehandled_access_fsright is set, then the domain can only communicate with matching sockets (OR scoped ones if the scope is set).Right, I see what you're saying, especially with the "additional access rules for other scopes" example, and I think I'm happy with this. I guess my attempt at trying to make the API more "elegant" would introduce complexity and also create future inconsistency if other existing scope bits are combined with
handled_accessrules.Quoted 7 lines
[...]
Anyway, we need to decide if this should be merged in Linux 7.0 (next week) or not. I'd prefer to merge it now because I think it works well and it's not a new concept wrt the abstract UNIX socket scoping. However, if there are any concern, I'd like to hear them now and I can delay this merge if needed. This patch series still need a new version but that should only be about cosmetic fixes. WDYT?I ended up being pretty busy today but I can definitely send the next version tomorrow with your formatting changes and comments. I'm happy with it going into the next merge window if you are. Justin raises a point about having these two mechanisms in the same ABI version - see below for consideration.
Quoted 11 lines
Quoted 1 line
[...]
My main concern is about user space libraries and users that may want to have conditional enforcement for compatibility reasons e.g., only enforce
LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET(let's say ABI v8) if it can also setLANDLOCK_ACCESS_FS_RESOLVE_UNIX(let's say ABI v9). I see two ways to deal with this case (if needed):
- add synthetic access right to easily let users "combine" two access rigths or none;
- have a more generic way to AND and OR access rights. I'm thinking about updating the Rust library in this direction.
Quoted 8 lines
I'm not sure I fully understand the complexity here, but I think, assuming these land in separate kernel versions, it will have to be that if both the scope bit and
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis requested (maybe if the user actually adds rules containingRESOLVE_UNIXaccess), but only the scope bit is supported, then it will have to skip enforcing pathname UNIX socket restrictions altogether by skipping both the scope bit and theRESOLVE_UNIXaccess (if in best effort mode), or fail (if in hard requirement mode).
Yeah, this should be OK in theory but it might be confusing to developers.
Quoted 6 lines
I don't immediately see how further customization ability (e.g. synthetic access rights or other AND/OR combination) could be used - if an app needs access to a privileged socket and can't pre-open it before
landlock_restrict_self(), then the only realistic choice is to not use the scope bits ifLANDLOCK_ACCESS_FS_RESOLVE_UNIXis not supported.
Yes, that's the main idea. The synthetic access right would just be useful to avoid doing this check each time but let the library do it instead. Anyway, that's mostly a (Rust) lib thing.
Quoted 11 lines · Justin Suess
Quoted 8 lines
Regardless if you merge the patch series now in 7.0 or a later version, I think there is something to be said about having the filesystem and scoped unix access right merged in the same ABI version / merge window.
As you pointed out earlier, the combination of the two flags is much flexible and useful to userspace consumers than one or the other, and if the features were merged separately, there would be an awkward middle ABI where user space consumers may have to make compromises or changes to sandbox between different versions or change application behavior.
[...]
Quoted 12 lines
Given that the scope bit and
RESOLVE_UNIXaccess right are in some sense part of the same system (they interact in an OR manner, after all), there is some positive for having them introduced in the same version, but on the other hand, with my above reasoning, I don't think these two mechanisms (scope bit andRESOLVE_UNIXaccess) being in different ABI versions would be too much of a problem. In either case, for applications which require access to more "privileged" sockets, when running on a kernel without theRESOLVE_UNIXaccess right support, no pathname socket restrictions can be applied (i.e. it won't use the scope bit either, there isn't much "compromise" it can make here). On the other hand, ifRESOLVE_UNIXis supported, then it knows that the scope bit is also supported, and can just use it.
Yes
Quoted 47 lines
Furthermore, an application / Landlock config etc can always opt to not use the scope bit at all, if it "knows" all the locations where the application's sockets would be placed, and just use
RESOLVE_UNIXaccess right (or nothing if it is not supported).(The following is a bit of a side note, not terribly relevant if we're deciding to go with the patch as is.)
Quoted 24 lines
Quoted 14 lines
[...]
Another way to put it is that, if FS-based and scope-based controls interacts in the above proposed way, both mechanisms feel like "poking holes" in the other. But as Mickaël said, one can think of the two mechanisms not as independent controls, but rather as two interfaces for the same control. The socket access control is "enabled" if either theLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess is handled, or the scope bit proposed in this patch is enabled.With that said, I can think of some alternative ways that might make this API look "better" (from a subjective point of view, feedback welcome), however it does mean more delays, and specifically, these will depend on
LANDLOCK_ACCESS_FS_RESOLVE_UNIX:Quoted 9 lines
One possibility is to simply always allow a Landlock domain to connect to its own sockets (in the case where
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled, otherwise all sockets are allowed). This might be reasonable, as one can only connect to a socket it creates if it has the permission to create it in the first place, which is already controlled byLANDLOCK_ACCESS_FS_MAKE_SOCK, so we don't really lose any policy flexibility here - if for some reason the sandboxer don't want to allow access to any (pathname) sockets, even the sandboxed app's own ones, it can just not allowLANDLOCK_ACCESS_FS_MAKE_SOCKanywhere.Quoted 3 lines
LANDLOCK_ACCESS_FS_MAKE_SOCKis only required to bind/listen to a socket, not to connect. I guess you was thinking aboutLANDLOCK_ACCESS_FS_RESOLVE_UNIXin this case?In this "allow same-scope connect unconditionally" proposal, the application would still be able to (bind to and) connect to its own sockets, even if
LANDLOCK_ACCESS_FS_RESOLVE_UNIXis handled and nothing is allowed to haveLANDLOCK_ACCESS_FS_RESOLVE_UNIXaccess. But a sandboxer which for whatever reason doesn't want this "allow same scope" default can still prevent the use of (pathname) sockets by restrictingLANDLOCK_ACCESS_FS_MAKE_SOCK, because if an app can't connect to any sockets it doesn't own, and can't create any sockets itself either, then it effectively can't connect to any sockets at all.
Quoted 4 lines
(Although on second thought, I guess there could be a case where an app first creates some socket files before doing
landlock_restrict_self(), then it might still be able to bind to these even withoutLANDLOCK_ACCESS_FS_MAKE_SOCK?)
That's good to keep in mind. There might be weird cases but developers should be encouraged to set all the scopes bits and then potentially allow specific sockets with FS_RESOLVE_UNIX.