Skip to content

Commit ae97330

Browse files
gnoackl0kod
authored andcommitted
landlock: Control pathname UNIX domain socket resolution by path
* Add a new access right LANDLOCK_ACCESS_FS_RESOLVE_UNIX, which controls the lookup operations for named UNIX domain sockets. The resolution happens during connect() and sendmsg() (depending on socket type). * Change access_mask_t from u16 to u32 (see below) * Hook into the path lookup in unix_find_bsd() in af_unix.c, using a LSM hook. Make policy decisions based on the new access rights * Increment the Landlock ABI version. * Minor test adaptations to keep the tests working. * Document the design rationale for scoped access rights, and cross-reference it from the header documentation. With this access right, access is granted if either of the following conditions is met: * The target socket's filesystem path was allow-listed using a LANDLOCK_RULE_PATH_BENEATH rule, *or*: * The target socket was created in the same Landlock domain in which LANDLOCK_ACCESS_FS_RESOLVE_UNIX was restricted. In case of a denial, connect() and sendmsg() return EACCES, which is the same error as it is returned if the user does not have the write bit in the traditional UNIX file system permissions of that file. The access_mask_t type grows from u16 to u32 to make space for the new access right. This also doubles the size of struct layer_access_masks from 32 byte to 64 byte. To avoid memory layout inconsistencies between architectures (especially m68k), pack and align struct access_masks [2]. Document the (possible future) interaction between scoped flags and other access rights in struct landlock_ruleset_attr, and summarize the rationale, as discussed in code review leading up to [3]. This feature was created with substantial discussion and input from Justin Suess, Tingmao Wang and Mickaël Salaün. Cc: Tingmao Wang <m@maowtm.org> Cc: Justin Suess <utilityemal77@gmail.com> Cc: Kuniyuki Iwashima <kuniyu@google.com> Suggested-by: Jann Horn <jannh@google.com> Link[1]: landlock-lsm/linux#36 Link[2]: https://lore.kernel.org/all/20260401.Re1Eesu1Yaij@digikod.net/ Link[3]: https://lore.kernel.org/all/20260205.8531e4005118@gnoack.org/ Signed-off-by: Günther Noack <gnoack3000@gmail.com> Acked-by: Sebastian Andrzej Siewior <bigeasy@linutronix.de> Link: https://lore.kernel.org/r/20260327164838.38231-5-gnoack3000@gmail.com [mic: Fix kernel-doc formatting, pack and align access_masks] Signed-off-by: Mickaël Salaün <mic@digikod.net>
1 parent 1c4fe87 commit ae97330

9 files changed

Lines changed: 200 additions & 9 deletions

File tree

Documentation/security/landlock.rst

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Landlock LSM: kernel documentation
77
==================================
88

99
:Author: Mickaël Salaün
10-
:Date: September 2025
10+
:Date: March 2026
1111

1212
Landlock's goal is to create scoped access-control (i.e. sandboxing). To
1313
harden a whole system, this feature should be available to any process,
@@ -89,6 +89,46 @@ this is required to keep access controls consistent over the whole system, and
8989
this avoids unattended bypasses through file descriptor passing (i.e. confused
9090
deputy attack).
9191

92+
.. _scoped-flags-interaction:
93+
94+
Interaction between scoped flags and other access rights
95+
--------------------------------------------------------
96+
97+
The ``scoped`` flags in &struct landlock_ruleset_attr restrict the
98+
use of *outgoing* IPC from the created Landlock domain, while they
99+
permit reaching out to IPC endpoints *within* the created Landlock
100+
domain.
101+
102+
In the future, scoped flags *may* interact with other access rights,
103+
e.g. so that abstract UNIX sockets can be allow-listed by name, or so
104+
that signals can be allow-listed by signal number or target process.
105+
106+
When introducing ``LANDLOCK_ACCESS_FS_RESOLVE_UNIX``, we defined it to
107+
implicitly have the same scoping semantics as a
108+
``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` flag would have: connecting to
109+
UNIX sockets within the same domain (where
110+
``LANDLOCK_ACCESS_FS_RESOLVE_UNIX`` is used) is unconditionally
111+
allowed.
112+
113+
The reasoning is:
114+
115+
* Like other IPC mechanisms, connecting to named UNIX sockets in the
116+
same domain should be expected and harmless. (If needed, users can
117+
further refine their Landlock policies with nested domains or by
118+
restricting ``LANDLOCK_ACCESS_FS_MAKE_SOCK``.)
119+
* We reserve the option to still introduce
120+
``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` in the future. (This would
121+
be useful if we wanted to have a Landlock rule to permit IPC access
122+
to other Landlock domains.)
123+
* But we can postpone the point in time when users have to deal with
124+
two interacting flags visible in the userspace API. (In particular,
125+
it is possible that it won't be needed in practice, in which case we
126+
can avoid the second flag altogether.)
127+
* If we *do* introduce ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` in the
128+
future, setting this scoped flag in a ruleset does *not reduce* the
129+
restrictions, because access within the same scope is already
130+
allowed based on ``LANDLOCK_ACCESS_FS_RESOLVE_UNIX``.
131+
92132
Tests
93133
=====
94134

include/uapi/linux/landlock.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,26 @@ struct landlock_net_port_attr {
250250
*
251251
* This access right is available since the fifth version of the Landlock
252252
* ABI.
253+
* - %LANDLOCK_ACCESS_FS_RESOLVE_UNIX: Look up pathname UNIX domain sockets
254+
* (:manpage:`unix(7)`). On UNIX domain sockets, this restricts both calls to
255+
* :manpage:`connect(2)` as well as calls to :manpage:`sendmsg(2)` with an
256+
* explicit recipient address.
257+
*
258+
* This access right only applies to connections to UNIX server sockets which
259+
* were created outside of the newly created Landlock domain (e.g. from within
260+
* a parent domain or from an unrestricted process). Newly created UNIX
261+
* servers within the same Landlock domain continue to be accessible. In this
262+
* regard, %LANDLOCK_ACCESS_FS_RESOLVE_UNIX has the same semantics as the
263+
* ``LANDLOCK_SCOPE_*`` flags.
264+
*
265+
* If a resolve attempt is denied, the operation returns an ``EACCES`` error,
266+
* in line with other filesystem access rights (but different to denials for
267+
* abstract UNIX domain sockets).
268+
*
269+
* This access right is available since the ninth version of the Landlock ABI.
270+
*
271+
* The rationale for this design is described in
272+
* :ref:`Documentation/security/landlock.rst <scoped-flags-interaction>`.
253273
*
254274
* Whether an opened file can be truncated with :manpage:`ftruncate(2)` or used
255275
* with `ioctl(2)` is determined during :manpage:`open(2)`, in the same way as
@@ -335,6 +355,7 @@ struct landlock_net_port_attr {
335355
#define LANDLOCK_ACCESS_FS_REFER (1ULL << 13)
336356
#define LANDLOCK_ACCESS_FS_TRUNCATE (1ULL << 14)
337357
#define LANDLOCK_ACCESS_FS_IOCTL_DEV (1ULL << 15)
358+
#define LANDLOCK_ACCESS_FS_RESOLVE_UNIX (1ULL << 16)
338359
/* clang-format on */
339360

340361
/**

security/landlock/access.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
LANDLOCK_ACCESS_FS_IOCTL_DEV)
3535
/* clang-format on */
3636

37-
typedef u16 access_mask_t;
37+
typedef u32 access_mask_t;
3838

3939
/* Makes sure all filesystem access rights can be stored. */
4040
static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_ACCESS_FS);
@@ -50,7 +50,7 @@ struct access_masks {
5050
access_mask_t fs : LANDLOCK_NUM_ACCESS_FS;
5151
access_mask_t net : LANDLOCK_NUM_ACCESS_NET;
5252
access_mask_t scope : LANDLOCK_NUM_SCOPE;
53-
};
53+
} __packed __aligned(sizeof(u32));
5454

5555
union access_masks_all {
5656
struct access_masks masks;

security/landlock/audit.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ static const char *const fs_access_strings[] = {
3737
[BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer",
3838
[BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate",
3939
[BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev",
40+
[BIT_INDEX(LANDLOCK_ACCESS_FS_RESOLVE_UNIX)] = "fs.resolve_unix",
4041
};
4142

4243
static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);

security/landlock/fs.c

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <linux/lsm_hooks.h>
2828
#include <linux/mount.h>
2929
#include <linux/namei.h>
30+
#include <linux/net.h>
3031
#include <linux/path.h>
3132
#include <linux/pid.h>
3233
#include <linux/rcupdate.h>
@@ -36,6 +37,7 @@
3637
#include <linux/types.h>
3738
#include <linux/wait_bit.h>
3839
#include <linux/workqueue.h>
40+
#include <net/af_unix.h>
3941
#include <uapi/linux/fiemap.h>
4042
#include <uapi/linux/landlock.h>
4143

@@ -314,7 +316,8 @@ static struct landlock_object *get_inode_object(struct inode *const inode)
314316
LANDLOCK_ACCESS_FS_WRITE_FILE | \
315317
LANDLOCK_ACCESS_FS_READ_FILE | \
316318
LANDLOCK_ACCESS_FS_TRUNCATE | \
317-
LANDLOCK_ACCESS_FS_IOCTL_DEV)
319+
LANDLOCK_ACCESS_FS_IOCTL_DEV | \
320+
LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
318321
/* clang-format on */
319322

320323
/*
@@ -1557,6 +1560,130 @@ static int hook_path_truncate(const struct path *const path)
15571560
return current_check_access_path(path, LANDLOCK_ACCESS_FS_TRUNCATE);
15581561
}
15591562

1563+
/**
1564+
* unmask_scoped_access - Remove access right bits in @masks in all layers
1565+
* where @client and @server have the same domain
1566+
*
1567+
* This does the same as domain_is_scoped(), but unmasks bits in @masks.
1568+
* It can not return early as domain_is_scoped() does.
1569+
*
1570+
* A scoped access for a given access right bit is allowed iff, for all layer
1571+
* depths where the access bit is set, the client and server domain are the
1572+
* same. This function clears the access rights @access in @masks at all layer
1573+
* depths where the client and server domain are the same, so that, when they
1574+
* are all cleared, the access is allowed.
1575+
*
1576+
* @client: Client domain
1577+
* @server: Server domain
1578+
* @masks: Layer access masks to unmask
1579+
* @access: Access bits that control scoping
1580+
*/
1581+
static void unmask_scoped_access(const struct landlock_ruleset *const client,
1582+
const struct landlock_ruleset *const server,
1583+
struct layer_access_masks *const masks,
1584+
const access_mask_t access)
1585+
{
1586+
int client_layer, server_layer;
1587+
const struct landlock_hierarchy *client_walker, *server_walker;
1588+
1589+
/* This should not happen. */
1590+
if (WARN_ON_ONCE(!client))
1591+
return;
1592+
1593+
/* Server has no Landlock domain; nothing to clear. */
1594+
if (!server)
1595+
return;
1596+
1597+
/*
1598+
* client_layer must be a signed integer with greater capacity
1599+
* than client->num_layers to ensure the following loop stops.
1600+
*/
1601+
BUILD_BUG_ON(sizeof(client_layer) > sizeof(client->num_layers));
1602+
1603+
client_layer = client->num_layers - 1;
1604+
client_walker = client->hierarchy;
1605+
server_layer = server->num_layers - 1;
1606+
server_walker = server->hierarchy;
1607+
1608+
/*
1609+
* Clears the access bits at all layers where the client domain is the
1610+
* same as the server domain. We start the walk at min(client_layer,
1611+
* server_layer). The layer bits until there can not be cleared because
1612+
* either the client or the server domain is missing.
1613+
*/
1614+
for (; client_layer > server_layer; client_layer--)
1615+
client_walker = client_walker->parent;
1616+
1617+
for (; server_layer > client_layer; server_layer--)
1618+
server_walker = server_walker->parent;
1619+
1620+
for (; client_layer >= 0; client_layer--) {
1621+
if (masks->access[client_layer] & access &&
1622+
client_walker == server_walker)
1623+
masks->access[client_layer] &= ~access;
1624+
1625+
client_walker = client_walker->parent;
1626+
server_walker = server_walker->parent;
1627+
}
1628+
}
1629+
1630+
static int hook_unix_find(const struct path *const path, struct sock *other,
1631+
int flags)
1632+
{
1633+
const struct landlock_ruleset *dom_other;
1634+
const struct landlock_cred_security *subject;
1635+
struct layer_access_masks layer_masks;
1636+
struct landlock_request request = {};
1637+
static const struct access_masks fs_resolve_unix = {
1638+
.fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
1639+
};
1640+
1641+
/* Lookup for the purpose of saving coredumps is OK. */
1642+
if (unlikely(flags & SOCK_COREDUMP))
1643+
return 0;
1644+
1645+
subject = landlock_get_applicable_subject(current_cred(),
1646+
fs_resolve_unix, NULL);
1647+
1648+
if (!subject)
1649+
return 0;
1650+
1651+
/*
1652+
* Ignoring return value: that the domains apply was already checked in
1653+
* landlock_get_applicable_subject() above.
1654+
*/
1655+
landlock_init_layer_masks(subject->domain, fs_resolve_unix.fs,
1656+
&layer_masks, LANDLOCK_KEY_INODE);
1657+
1658+
/* Checks the layers in which we are connecting within the same domain. */
1659+
unix_state_lock(other);
1660+
if (unlikely(sock_flag(other, SOCK_DEAD) || !other->sk_socket ||
1661+
!other->sk_socket->file)) {
1662+
unix_state_unlock(other);
1663+
/*
1664+
* We rely on the caller to catch the (non-reversible) SOCK_DEAD
1665+
* condition and retry the lookup. If we returned an error
1666+
* here, the lookup would not get retried.
1667+
*/
1668+
return 0;
1669+
}
1670+
dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain;
1671+
1672+
/* Access to the same (or a lower) domain is always allowed. */
1673+
unmask_scoped_access(subject->domain, dom_other, &layer_masks,
1674+
fs_resolve_unix.fs);
1675+
unix_state_unlock(other);
1676+
1677+
/* Checks the connections to allow-listed paths. */
1678+
if (is_access_to_paths_allowed(subject->domain, path,
1679+
fs_resolve_unix.fs, &layer_masks,
1680+
&request, NULL, 0, NULL, NULL, NULL))
1681+
return 0;
1682+
1683+
landlock_log_denial(subject, &request);
1684+
return -EACCES;
1685+
}
1686+
15601687
/* File hooks */
15611688

15621689
/**
@@ -1834,6 +1961,7 @@ static struct security_hook_list landlock_hooks[] __ro_after_init = {
18341961
LSM_HOOK_INIT(path_unlink, hook_path_unlink),
18351962
LSM_HOOK_INIT(path_rmdir, hook_path_rmdir),
18361963
LSM_HOOK_INIT(path_truncate, hook_path_truncate),
1964+
LSM_HOOK_INIT(unix_find, hook_unix_find),
18371965

18381966
LSM_HOOK_INIT(file_alloc_security, hook_file_alloc_security),
18391967
LSM_HOOK_INIT(file_open, hook_file_open),

security/landlock/limits.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
#define LANDLOCK_MAX_NUM_LAYERS 16
2020
#define LANDLOCK_MAX_NUM_RULES U32_MAX
2121

22-
#define LANDLOCK_LAST_ACCESS_FS LANDLOCK_ACCESS_FS_IOCTL_DEV
22+
#define LANDLOCK_LAST_ACCESS_FS LANDLOCK_ACCESS_FS_RESOLVE_UNIX
2323
#define LANDLOCK_MASK_ACCESS_FS ((LANDLOCK_LAST_ACCESS_FS << 1) - 1)
2424
#define LANDLOCK_NUM_ACCESS_FS __const_hweight64(LANDLOCK_MASK_ACCESS_FS)
2525

security/landlock/syscalls.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ static const struct file_operations ruleset_fops = {
166166
* If the change involves a fix that requires userspace awareness, also update
167167
* the errata documentation in Documentation/userspace-api/landlock.rst .
168168
*/
169-
const int landlock_abi_version = 8;
169+
const int landlock_abi_version = 9;
170170

171171
/**
172172
* sys_landlock_create_ruleset - Create a new ruleset

tools/testing/selftests/landlock/base_test.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ TEST(abi_version)
7676
const struct landlock_ruleset_attr ruleset_attr = {
7777
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
7878
};
79-
ASSERT_EQ(8, landlock_create_ruleset(NULL, 0,
79+
ASSERT_EQ(9, landlock_create_ruleset(NULL, 0,
8080
LANDLOCK_CREATE_RULESET_VERSION));
8181

8282
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,

tools/testing/selftests/landlock/fs_test.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,9 +575,10 @@ TEST_F_FORK(layout1, inval)
575575
LANDLOCK_ACCESS_FS_WRITE_FILE | \
576576
LANDLOCK_ACCESS_FS_READ_FILE | \
577577
LANDLOCK_ACCESS_FS_TRUNCATE | \
578-
LANDLOCK_ACCESS_FS_IOCTL_DEV)
578+
LANDLOCK_ACCESS_FS_IOCTL_DEV | \
579+
LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
579580

580-
#define ACCESS_LAST LANDLOCK_ACCESS_FS_IOCTL_DEV
581+
#define ACCESS_LAST LANDLOCK_ACCESS_FS_RESOLVE_UNIX
581582

582583
#define ACCESS_ALL ( \
583584
ACCESS_FILE | \

0 commit comments

Comments
 (0)