2024-03-28 23:12:33

by Tahera Fahimi

[permalink] [raw]
Subject: [PATCH v2] landlock: Add abstract unix socket connect restrictions

Abstract unix sockets are used for local interprocess communication without
relying on filesystem. Since landlock has no restriction for connecting to
a UNIX socket in the abstract namespace, a sandboxed process can connect to
a socket outside the sandboxed environment. Access to such sockets should
be scoped the same way ptrace access is limited.

For a landlocked process to be allowed to connect to a target process, it
must have a subset of the target process’s rules (the connecting socket
must be in a sub-domain of the listening socket). This patch adds a new
LSM hook for connect function in unix socket with the related access rights.

Link to first draft:
https://lore.kernel.org/outreachy/[email protected]/

Signed-off-by: Tahera Fahimi <[email protected]>

----
Changes in v2:
- Remove wrapper functions, noted by Casey Schaufler <[email protected]>
---
security/landlock/task.c | 40 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 40 insertions(+)

diff --git a/security/landlock/task.c b/security/landlock/task.c
index 849f5123610b..67528f87b7de 100644
--- a/security/landlock/task.c
+++ b/security/landlock/task.c
@@ -13,6 +13,7 @@
#include <linux/lsm_hooks.h>
#include <linux/rcupdate.h>
#include <linux/sched.h>
+#include <net/sock.h>

#include "common.h"
#include "cred.h"
@@ -108,9 +109,48 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
return task_ptrace(parent, current);
}

+static bool unix_sock_is_scoped(struct sock *const sock,
+ struct sock *const other)
+{
+ bool is_scoped = true;
+
+ /* get the ruleset of connecting sock*/
+ const struct landlock_ruleset *const dom_sock =
+ landlock_get_current_domain();
+
+ if (!dom_sock)
+ return true;
+
+ /* get credential of listening sock*/
+ const struct cred *cred_other = get_cred(other->sk_peer_cred);
+
+ if (!cred_other)
+ return true;
+
+ /* retrieve the landlock_rulesets */
+ const struct landlock_ruleset *dom_parent;
+
+ rcu_read_lock();
+ dom_parent = landlock_cred(cred_other)->domain;
+ is_scoped = domain_scope_le(dom_parent, dom_sock);
+ rcu_read_unlock();
+
+ return is_scoped;
+}
+
+static int hook_unix_stream_connect(struct sock *const sock,
+ struct sock *const other,
+ struct sock *const newsk)
+{
+ if (unix_sock_is_scoped(sock, other))
+ return 0;
+ return -EPERM;
+}
+
static struct security_hook_list landlock_hooks[] __ro_after_init = {
LSM_HOOK_INIT(ptrace_access_check, hook_ptrace_access_check),
LSM_HOOK_INIT(ptrace_traceme, hook_ptrace_traceme),
+ LSM_HOOK_INIT(unix_stream_connect, hook_unix_stream_connect),
};

__init void landlock_add_task_hooks(void)
--
2.34.1



2024-04-02 09:53:34

by Mickaël Salaün

[permalink] [raw]
Subject: Re: [PATCH v2] landlock: Add abstract unix socket connect restrictions

Thanks for this patch. Please CC the netdev mailing list too, they may
be interested by this feature. I also added a few folks that previously
showed their interest for this feature.

On Thu, Mar 28, 2024 at 05:12:13PM -0600, TaheraFahimi wrote:
> Abstract unix sockets are used for local interprocess communication without
> relying on filesystem. Since landlock has no restriction for connecting to
> a UNIX socket in the abstract namespace, a sandboxed process can connect to
> a socket outside the sandboxed environment. Access to such sockets should
> be scoped the same way ptrace access is limited.

This is good but it would be better to explain that Landlock doesn't
currently control abstract unix sockets and that it would make sense for
a sandbox.


>
> For a landlocked process to be allowed to connect to a target process, it
> must have a subset of the target process’s rules (the connecting socket
> must be in a sub-domain of the listening socket). This patch adds a new
> LSM hook for connect function in unix socket with the related access rights.

Because of compatibility reasons, and because Landlock should be
flexible, we need to extend the user space interface. As explained in
the GitHub issue, we need to add a new "scoped" field to the
landlock_ruleset_attr struct. This field will optionally contain a
LANDLOCK_RULESET_SCOPED_ABSTRACT_UNIX_SOCKET flag to specify that this
ruleset will deny any connection from within the sandbox to its parents
(i.e. any parent sandbox or not-sandboxed processes).

>
> Link to first draft:
> https://lore.kernel.org/outreachy/[email protected]/

You can move this sentence in the below changelog.

>

You can add this:

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

> Signed-off-by: Tahera Fahimi <[email protected]>

Your Git (or email app) configuration doesn't use the same name as here.

Please run ./scripts/checkpatch.pl on this patch and fix the warnings.

>
> ----
> Changes in v2:
> - Remove wrapper functions, noted by Casey Schaufler <[email protected]>
> ---
> security/landlock/task.c | 40 ++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 40 insertions(+)
>
> diff --git a/security/landlock/task.c b/security/landlock/task.c
> index 849f5123610b..67528f87b7de 100644
> --- a/security/landlock/task.c
> +++ b/security/landlock/task.c
> @@ -13,6 +13,7 @@
> #include <linux/lsm_hooks.h>
> #include <linux/rcupdate.h>
> #include <linux/sched.h>
> +#include <net/sock.h>
>
> #include "common.h"
> #include "cred.h"
> @@ -108,9 +109,48 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
> return task_ptrace(parent, current);
> }
>
> +static bool unix_sock_is_scoped(struct sock *const sock,

For consistency with task_is_scoped(), you can rename this to
sock_is_scoped().

> + struct sock *const other)
> +{
> + bool is_scoped = true;
> +
> + /* get the ruleset of connecting sock*/

These comments don't help more than the following line, you can remove
them.

> + const struct landlock_ruleset *const dom_sock =

According to the name it looks like the domain of the socket but it is
just the domain of the current task. Just "dom" would be clearer and
more consistent with security/landlock/fs.c

> + landlock_get_current_domain();
> +
> + if (!dom_sock)
> + return true;
> +
> + /* get credential of listening sock*/
> + const struct cred *cred_other = get_cred(other->sk_peer_cred);

We have a get but not a put call, so the credentials will never be
freed. The put call must be called before any return, so you
probably need to follow the goto/error pattern.

In the context of these LSM hooks, only unix_listen() sets the "other"
socket credential, and unix_listen() is guarded by unix_state_lock()
which locks unix_sk(s)->lock . When security_unix_stream_connect() or
security_unix_may_send() are called, unix_sk(s)->lock is locked as well,
which protects the credentials against race-conditions (TOCTOU:
time-of-check to time-of-use). We should then make that explicit with
this assertion (which also documents it):

lockdep_assert_held(&unix_sk(other)->lock);

In theory it is then not required to call get_cred(). However, because
the performance impact should be negligible and to avoid a potential
use-after-free (not possible in theory with the current code), it would
be safer to still call get/put. It would be worse to have a
use-after-free rather than an access control issue.

Another thing to keep in mind is that for this hook to be
race-condition-free, the credential must not change anyway. A comment
should highlight that.

> +
> + if (!cred_other)
> + return true;
> +
> + /* retrieve the landlock_rulesets */
> + const struct landlock_ruleset *dom_parent;

All declarations should be at the top of functions.

> +
> + rcu_read_lock();

No need for this RCU lock because the lock is managed by
unix_state_lock() in this case.

> + dom_parent = landlock_cred(cred_other)->domain;
> + is_scoped = domain_scope_le(dom_parent, dom_sock);
> + rcu_read_unlock();
> +
> + return is_scoped;
> +}
> +
> +static int hook_unix_stream_connect(struct sock *const sock,
> + struct sock *const other,
> + struct sock *const newsk)
> +{
> + if (unix_sock_is_scoped(sock, other))
> + return 0;
> + return -EPERM;
> +}
> +
> static struct security_hook_list landlock_hooks[] __ro_after_init = {
> LSM_HOOK_INIT(ptrace_access_check, hook_ptrace_access_check),
> LSM_HOOK_INIT(ptrace_traceme, hook_ptrace_traceme),
> + LSM_HOOK_INIT(unix_stream_connect, hook_unix_stream_connect),

Please add a hook for security_unix_may_send() too, it should be quite
similar, and simplify the patch's subject accordingly.

You now need to add tests (in a dedicated patch) extending
tools/testing/selftests/landlock/ptrace_test.c (I'll rename the file
later).

These tests should also check with unnamed and named unix sockets. I
guess the current code doesn't differentiate them and control all kind
of unix sockets. Because they must explicitly be passed, sockets
created with socketpair(2) (i.e. unnamed socket) should never be denied.

> };
>
> __init void landlock_add_task_hooks(void)
> --
> 2.34.1
>
>

2024-04-10 22:26:59

by Tahera Fahimi

[permalink] [raw]
Subject: Re: [PATCH v2] landlock: Add abstract unix socket connect restrictions

On Tue, Apr 02, 2024 at 11:53:09AM +0200, Mickaël Salaün wrote:
> Thanks for this patch. Please CC the netdev mailing list too, they may
> be interested by this feature. I also added a few folks that previously
> showed their interest for this feature.
>
> On Thu, Mar 28, 2024 at 05:12:13PM -0600, TaheraFahimi wrote:
> > Abstract unix sockets are used for local interprocess communication without
> > relying on filesystem. Since landlock has no restriction for connecting to
> > a UNIX socket in the abstract namespace, a sandboxed process can connect to
> > a socket outside the sandboxed environment. Access to such sockets should
> > be scoped the same way ptrace access is limited.
>
> This is good but it would be better to explain that Landlock doesn't
> currently control abstract unix sockets and that it would make sense for
> a sandbox.
>
>
> >
> > For a landlocked process to be allowed to connect to a target process, it
> > must have a subset of the target process’s rules (the connecting socket
> > must be in a sub-domain of the listening socket). This patch adds a new
> > LSM hook for connect function in unix socket with the related access rights.
>
> Because of compatibility reasons, and because Landlock should be
> flexible, we need to extend the user space interface. As explained in
> the GitHub issue, we need to add a new "scoped" field to the
> landlock_ruleset_attr struct. This field will optionally contain a
> LANDLOCK_RULESET_SCOPED_ABSTRACT_UNIX_SOCKET flag to specify that this
> ruleset will deny any connection from within the sandbox to its parents
> (i.e. any parent sandbox or not-sandboxed processes).
Thanks for the feedback. Here is what I understood, please correct me if
I am wrong. First, I should add another field to the
landlock_ruleset_attr (a field like handled_access_net, but for the unix
sockets) with a flag LANDLOCK_ACCESS_UNIX_CONNECT (it is a flag like
LANDLOCK_ACCESS_NET_CONNECT_TCP but fot the unix sockets connect).

> >
> > Link to first draft:
> > https://lore.kernel.org/outreachy/[email protected]/
>
> You can move this sentence in the below changelog.
>
> >
>
> You can add this:
>
> Closes: https://github.com/landlock-lsm/linux/issues/7
>
> > Signed-off-by: Tahera Fahimi <[email protected]>
>
> Your Git (or email app) configuration doesn't use the same name as here.
>
> Please run ./scripts/checkpatch.pl on this patch and fix the warnings.
>
> >
> > ----
> > Changes in v2:
> > - Remove wrapper functions, noted by Casey Schaufler <[email protected]>
> > ---
> > security/landlock/task.c | 40 ++++++++++++++++++++++++++++++++++++++++
> > 1 file changed, 40 insertions(+)
> >
> > diff --git a/security/landlock/task.c b/security/landlock/task.c
> > index 849f5123610b..67528f87b7de 100644
> > --- a/security/landlock/task.c
> > +++ b/security/landlock/task.c
> > @@ -13,6 +13,7 @@
> > #include <linux/lsm_hooks.h>
> > #include <linux/rcupdate.h>
> > #include <linux/sched.h>
> > +#include <net/sock.h>
> >
> > #include "common.h"
> > #include "cred.h"
> > @@ -108,9 +109,48 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
> > return task_ptrace(parent, current);
> > }
> >
> > +static bool unix_sock_is_scoped(struct sock *const sock,
>
> For consistency with task_is_scoped(), you can rename this to
> sock_is_scoped().
>
> > + struct sock *const other)
> > +{
> > + bool is_scoped = true;
> > +
> > + /* get the ruleset of connecting sock*/
>
> These comments don't help more than the following line, you can remove
> them.
>
> > + const struct landlock_ruleset *const dom_sock =
>
> According to the name it looks like the domain of the socket but it is
> just the domain of the current task. Just "dom" would be clearer and
> more consistent with security/landlock/fs.c
>
> > + landlock_get_current_domain();
> > +
> > + if (!dom_sock)
> > + return true;
> > +
> > + /* get credential of listening sock*/
> > + const struct cred *cred_other = get_cred(other->sk_peer_cred);
>
> We have a get but not a put call, so the credentials will never be
> freed. The put call must be called before any return, so you
> probably need to follow the goto/error pattern.
>
> In the context of these LSM hooks, only unix_listen() sets the "other"
> socket credential, and unix_listen() is guarded by unix_state_lock()
> which locks unix_sk(s)->lock . When security_unix_stream_connect() or
> security_unix_may_send() are called, unix_sk(s)->lock is locked as well,
> which protects the credentials against race-conditions (TOCTOU:
> time-of-check to time-of-use). We should then make that explicit with
> this assertion (which also documents it):
>
> lockdep_assert_held(&unix_sk(other)->lock);
>
> In theory it is then not required to call get_cred(). However, because
> the performance impact should be negligible and to avoid a potential
> use-after-free (not possible in theory with the current code), it would
> be safer to still call get/put. It would be worse to have a
> use-after-free rather than an access control issue.
>
> Another thing to keep in mind is that for this hook to be
> race-condition-free, the credential must not change anyway. A comment
> should highlight that.
>
> > +
> > + if (!cred_other)
> > + return true;
> > +
> > + /* retrieve the landlock_rulesets */
> > + const struct landlock_ruleset *dom_parent;
>
> All declarations should be at the top of functions.
>
> > +
> > + rcu_read_lock();
>
> No need for this RCU lock because the lock is managed by
> unix_state_lock() in this case.
>
> > + dom_parent = landlock_cred(cred_other)->domain;
> > + is_scoped = domain_scope_le(dom_parent, dom_sock);
> > + rcu_read_unlock();
> > +
> > + return is_scoped;
> > +}
> > +
> > +static int hook_unix_stream_connect(struct sock *const sock,
> > + struct sock *const other,
> > + struct sock *const newsk)
> > +{
> > + if (unix_sock_is_scoped(sock, other))
> > + return 0;
> > + return -EPERM;
> > +}
> > +
> > static struct security_hook_list landlock_hooks[] __ro_after_init = {
> > LSM_HOOK_INIT(ptrace_access_check, hook_ptrace_access_check),
> > LSM_HOOK_INIT(ptrace_traceme, hook_ptrace_traceme),
> > + LSM_HOOK_INIT(unix_stream_connect, hook_unix_stream_connect),
>
> Please add a hook for security_unix_may_send() too, it should be quite
> similar, and simplify the patch's subject accordingly.
>
> You now need to add tests (in a dedicated patch) extending
> tools/testing/selftests/landlock/ptrace_test.c (I'll rename the file
> later).
>
> These tests should also check with unnamed and named unix sockets. I
> guess the current code doesn't differentiate them and control all kind
> of unix sockets. Because they must explicitly be passed, sockets
> created with socketpair(2) (i.e. unnamed socket) should never be denied.
>
> > };
> >
> > __init void landlock_add_task_hooks(void)
> > --
> > 2.34.1
> >
> >