Landlock update for v6.15-rc1

-----BEGIN PGP SIGNATURE-----
 
 iIYEABYKAC4WIQSVyBthFV4iTW/VU1/l49DojIL20gUCZ+bGgBAcbWljQGRpZ2lr
 b2QubmV0AAoJEOXj0OiMgvbSKmgBAICZsmQTuKMHIXdB7kwA+BX5k++SZcyA+qHN
 0hrJTSMsAP0Uv6NpiPT4CTduqBMRbuMwNhujBczRiok6yaHDbC8eCw==
 =K8XL
 -----END PGP SIGNATURE-----

Merge tag 'landlock-6.15-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux

Pull landlock updates from Mickaël Salaün:
 "This brings two main changes to Landlock:

   - A signal scoping fix with a new interface for user space to know if
     it is compatible with the running kernel.

   - Audit support to give visibility on why access requests are denied,
     including the origin of the security policy, missing access rights,
     and description of object(s). This was designed to limit log spam
     as much as possible while still alerting about unexpected blocked
     access.

  With these changes come new and improved documentation, and a lot of
  new tests"

* tag 'landlock-6.15-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux: (36 commits)
  landlock: Add audit documentation
  selftests/landlock: Add audit tests for network
  selftests/landlock: Add audit tests for filesystem
  selftests/landlock: Add audit tests for abstract UNIX socket scoping
  selftests/landlock: Add audit tests for ptrace
  selftests/landlock: Test audit with restrict flags
  selftests/landlock: Add tests for audit flags and domain IDs
  selftests/landlock: Extend tests for landlock_restrict_self(2)'s flags
  selftests/landlock: Add test for invalid ruleset file descriptor
  samples/landlock: Enable users to log sandbox denials
  landlock: Add LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF
  landlock: Add LANDLOCK_RESTRICT_SELF_LOG_*_EXEC_* flags
  landlock: Log scoped denials
  landlock: Log TCP bind and connect denials
  landlock: Log truncate and IOCTL denials
  landlock: Factor out IOCTL hooks
  landlock: Log file-related denials
  landlock: Log mount-related denials
  landlock: Add AUDIT_LANDLOCK_DOMAIN and log domain status
  landlock: Add AUDIT_LANDLOCK_ACCESS and log ptrace denials
  ...
This commit is contained in:
Linus Torvalds 2025-03-28 12:37:13 -07:00
commit 7288511606
48 changed files with 4972 additions and 327 deletions

View File

@ -48,3 +48,4 @@ subdirectories.
Yama
SafeSetID
ipe
landlock

View File

@ -0,0 +1,158 @@
.. SPDX-License-Identifier: GPL-2.0
.. Copyright © 2025 Microsoft Corporation
================================
Landlock: system-wide management
================================
:Author: Mickaël Salaün
:Date: March 2025
Landlock can leverage the audit framework to log events.
User space documentation can be found here:
Documentation/userspace-api/landlock.rst.
Audit
=====
Denied access requests are logged by default for a sandboxed program if `audit`
is enabled. This default behavior can be changed with the
sys_landlock_restrict_self() flags (cf.
Documentation/userspace-api/landlock.rst). Landlock logs can also be masked
thanks to audit rules. Landlock can generate 2 audit record types.
Record types
------------
AUDIT_LANDLOCK_ACCESS
This record type identifies a denied access request to a kernel resource.
The ``domain`` field indicates the ID of the domain that blocked the
request. The ``blockers`` field indicates the cause(s) of this denial
(separated by a comma), and the following fields identify the kernel object
(similar to SELinux). There may be more than one of this record type per
audit event.
Example with a file link request generating two records in the same event::
domain=195ba459b blockers=fs.refer path="/usr/bin" dev="vda2" ino=351
domain=195ba459b blockers=fs.make_reg,fs.refer path="/usr/local" dev="vda2" ino=365
AUDIT_LANDLOCK_DOMAIN
This record type describes the status of a Landlock domain. The ``status``
field can be either ``allocated`` or ``deallocated``.
The ``allocated`` status is part of the same audit event and follows
the first logged ``AUDIT_LANDLOCK_ACCESS`` record of a domain. It identifies
Landlock domain information at the time of the sys_landlock_restrict_self()
call with the following fields:
- the ``domain`` ID
- the enforcement ``mode``
- the domain creator's ``pid``
- the domain creator's ``uid``
- the domain creator's executable path (``exe``)
- the domain creator's command line (``comm``)
Example::
domain=195ba459b status=allocated mode=enforcing pid=300 uid=0 exe="/root/sandboxer" comm="sandboxer"
The ``deallocated`` status is an event on its own and it identifies a
Landlock domain release. After such event, it is guarantee that the
related domain ID will never be reused during the lifetime of the system.
The ``domain`` field indicates the ID of the domain which is released, and
the ``denials`` field indicates the total number of denied access request,
which might not have been logged according to the audit rules and
sys_landlock_restrict_self()'s flags.
Example::
domain=195ba459b status=deallocated denials=3
Event samples
--------------
Here are two examples of log events (see serial numbers).
In this example a sandboxed program (``kill``) tries to send a signal to the
init process, which is denied because of the signal scoping restriction
(``LL_SCOPED=s``)::
$ LL_FS_RO=/ LL_FS_RW=/ LL_SCOPED=s LL_FORCE_LOG=1 ./sandboxer kill 1
This command generates two events, each identified with a unique serial
number following a timestamp (``msg=audit(1729738800.268:30)``). The first
event (serial ``30``) contains 4 records. The first record
(``type=LANDLOCK_ACCESS``) shows an access denied by the domain `1a6fdc66f`.
The cause of this denial is signal scopping restriction
(``blockers=scope.signal``). The process that would have receive this signal
is the init process (``opid=1 ocomm="systemd"``).
The second record (``type=LANDLOCK_DOMAIN``) describes (``status=allocated``)
domain `1a6fdc66f`. This domain was created by process ``286`` executing the
``/root/sandboxer`` program launched by the root user.
The third record (``type=SYSCALL``) describes the syscall, its provided
arguments, its result (``success=no exit=-1``), and the process that called it.
The fourth record (``type=PROCTITLE``) shows the command's name as an
hexadecimal value. This can be translated with ``python -c
'print(bytes.fromhex("6B696C6C0031"))'``.
Finally, the last record (``type=LANDLOCK_DOMAIN``) is also the only one from
the second event (serial ``31``). It is not tied to a direct user space action
but an asynchronous one to free resources tied to a Landlock domain
(``status=deallocated``). This can be useful to know that the following logs
will not concern the domain ``1a6fdc66f`` anymore. This record also summarize
the number of requests this domain denied (``denials=1``), whether they were
logged or not.
.. code-block::
type=LANDLOCK_ACCESS msg=audit(1729738800.268:30): domain=1a6fdc66f blockers=scope.signal opid=1 ocomm="systemd"
type=LANDLOCK_DOMAIN msg=audit(1729738800.268:30): domain=1a6fdc66f status=allocated mode=enforcing pid=286 uid=0 exe="/root/sandboxer" comm="sandboxer"
type=SYSCALL msg=audit(1729738800.268:30): arch=c000003e syscall=62 success=no exit=-1 [..] ppid=272 pid=286 auid=0 uid=0 gid=0 [...] comm="kill" [...]
type=PROCTITLE msg=audit(1729738800.268:30): proctitle=6B696C6C0031
type=LANDLOCK_DOMAIN msg=audit(1729738800.324:31): domain=1a6fdc66f status=deallocated denials=1
Here is another example showcasing filesystem access control::
$ LL_FS_RO=/ LL_FS_RW=/tmp LL_FORCE_LOG=1 ./sandboxer sh -c "echo > /etc/passwd"
The related audit logs contains 8 records from 3 different events (serials 33,
34 and 35) created by the same domain `1a6fdc679`::
type=LANDLOCK_ACCESS msg=audit(1729738800.221:33): domain=1a6fdc679 blockers=fs.write_file path="/dev/tty" dev="devtmpfs" ino=9
type=LANDLOCK_DOMAIN msg=audit(1729738800.221:33): domain=1a6fdc679 status=allocated mode=enforcing pid=289 uid=0 exe="/root/sandboxer" comm="sandboxer"
type=SYSCALL msg=audit(1729738800.221:33): arch=c000003e syscall=257 success=no exit=-13 [...] ppid=272 pid=289 auid=0 uid=0 gid=0 [...] comm="sh" [...]
type=PROCTITLE msg=audit(1729738800.221:33): proctitle=7368002D63006563686F203E202F6574632F706173737764
type=LANDLOCK_ACCESS msg=audit(1729738800.221:34): domain=1a6fdc679 blockers=fs.write_file path="/etc/passwd" dev="vda2" ino=143821
type=SYSCALL msg=audit(1729738800.221:34): arch=c000003e syscall=257 success=no exit=-13 [...] ppid=272 pid=289 auid=0 uid=0 gid=0 [...] comm="sh" [...]
type=PROCTITLE msg=audit(1729738800.221:34): proctitle=7368002D63006563686F203E202F6574632F706173737764
type=LANDLOCK_DOMAIN msg=audit(1729738800.261:35): domain=1a6fdc679 status=deallocated denials=2
Event filtering
---------------
If you get spammed with audit logs related to Landlock, this is either an
attack attempt or a bug in the security policy. We can put in place some
filters to limit noise with two complementary ways:
- with sys_landlock_restrict_self()'s flags if we can fix the sandboxed
programs,
- or with audit rules (see :manpage:`auditctl(8)`).
Additional documentation
========================
* `Linux Audit Documentation`_
* Documentation/userspace-api/landlock.rst
* Documentation/security/landlock.rst
* https://landlock.io
.. Links
.. _Linux Audit Documentation:
https://github.com/linux-audit/audit-documentation/wiki

View File

@ -7,7 +7,7 @@ Landlock LSM: kernel documentation
==================================
:Author: Mickaël Salaün
:Date: December 2022
:Date: March 2025
Landlock's goal is to create scoped access-control (i.e. sandboxing). To
harden a whole system, this feature should be available to any process,
@ -45,6 +45,10 @@ Guiding principles for safe access controls
sandboxed process shall retain their scoped accesses (at the time of resource
acquisition) whatever process uses them.
Cf. `File descriptor access rights`_.
* Access denials shall be logged according to system and Landlock domain
configurations. Log entries must contain information about the cause of the
denial and the owner of the related security policy. Such log generation
should have a negligible performance and memory impact on allowed requests.
Design choices
==============
@ -124,6 +128,13 @@ makes the reasoning much easier and helps avoid pitfalls.
.. kernel-doc:: security/landlock/ruleset.h
:identifiers:
Additional documentation
========================
* Documentation/userspace-api/landlock.rst
* Documentation/admin-guide/LSM/landlock.rst
* https://landlock.io
.. Links
.. _tools/testing/selftests/landlock/:
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/tools/testing/selftests/landlock/

View File

@ -8,7 +8,7 @@ Landlock: unprivileged access control
=====================================
:Author: Mickaël Salaün
:Date: January 2025
:Date: March 2025
The goal of Landlock is to enable restriction of ambient rights (e.g. global
filesystem or network access) for a set of processes. Because Landlock
@ -317,33 +317,32 @@ IPC scoping
-----------
Similar to the implicit `Ptrace restrictions`_, we may want to further restrict
interactions between sandboxes. Each Landlock domain can be explicitly scoped
for a set of actions by specifying it on a ruleset. For example, if a
sandboxed process should not be able to :manpage:`connect(2)` to a
non-sandboxed process through abstract :manpage:`unix(7)` sockets, we can
specify such a restriction with ``LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET``.
Moreover, if a sandboxed process should not be able to send a signal to a
non-sandboxed process, we can specify this restriction with
``LANDLOCK_SCOPE_SIGNAL``.
interactions between sandboxes. Therefore, at ruleset creation time, each
Landlock domain can restrict the scope for certain operations, so that these
operations can only reach out to processes within the same Landlock domain or in
a nested Landlock domain (the "scope").
A sandboxed process can connect to a non-sandboxed process when its domain is
not scoped. If a process's domain is scoped, it can only connect to sockets
created by processes in the same scope.
Moreover, if a process is scoped to send signal to a non-scoped process, it can
only send signals to processes in the same scope.
The operations which can be scoped are:
A connected datagram socket behaves like a stream socket when its domain is
scoped, meaning if the domain is scoped after the socket is connected, it can
still :manpage:`send(2)` data just like a stream socket. However, in the same
scenario, a non-connected datagram socket cannot send data (with
:manpage:`sendto(2)`) outside its scope.
``LANDLOCK_SCOPE_SIGNAL``
This limits the sending of signals to target processes which run within the
same or a nested Landlock domain.
A process with a scoped domain can inherit a socket created by a non-scoped
process. The process cannot connect to this socket since it has a scoped
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.
IPC scoping does not support exceptions, so if a domain is scoped, no rules can
be added to allow access to resources or processes outside of the scope.
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
remote end does not stem from the same or a nested Landlock domain.
A :manpage:`sendto(2)` on a socket which was previously connected will not
be restricted. This works for both datagram and stream sockets.
IPC scoping does not support exceptions via :manpage:`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.
Truncating files
----------------
@ -595,6 +594,16 @@ Starting with the Landlock ABI version 6, it is possible to restrict
:manpage:`signal(7)` sending by setting ``LANDLOCK_SCOPE_SIGNAL`` to the
``scoped`` ruleset attribute.
Logging (ABI < 7)
-----------------
Starting with the Landlock ABI version 7, it is possible to control logging of
Landlock audit events with the ``LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF``,
``LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON``, and
``LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF`` flags passed to
sys_landlock_restrict_self(). See Documentation/admin-guide/LSM/landlock.rst
for more details on audit.
.. _kernel_support:
Kernel support
@ -683,9 +692,16 @@ fine-grained restrictions). Moreover, their complexity can lead to security
issues, especially when untrusted processes can manipulate them (cf.
`Controlling access to user namespaces <https://lwn.net/Articles/673597/>`_).
How to disable Landlock audit records?
--------------------------------------
You might want to put in place filters as explained here:
Documentation/admin-guide/LSM/landlock.rst
Additional documentation
========================
* Documentation/admin-guide/LSM/landlock.rst
* Documentation/security/landlock.rst
* https://landlock.io

View File

@ -13157,6 +13157,7 @@ L: linux-security-module@vger.kernel.org
S: Supported
W: https://landlock.io
T: git https://git.kernel.org/pub/scm/linux/kernel/git/mic/linux.git
F: Documentation/admin-guide/LSM/landlock.rst
F: Documentation/security/landlock.rst
F: Documentation/userspace-api/landlock.rst
F: fs/ioctl.c

View File

@ -132,6 +132,9 @@ void common_lsm_audit(struct common_audit_data *a,
void (*pre_audit)(struct audit_buffer *, void *),
void (*post_audit)(struct audit_buffer *, void *));
void audit_log_lsm_data(struct audit_buffer *ab,
const struct common_audit_data *a);
#else /* CONFIG_AUDIT */
static inline void common_lsm_audit(struct common_audit_data *a,
@ -140,6 +143,11 @@ static inline void common_lsm_audit(struct common_audit_data *a,
{
}
static inline void audit_log_lsm_data(struct audit_buffer *ab,
const struct common_audit_data *a)
{
}
#endif /* CONFIG_AUDIT */
#endif

View File

@ -33,7 +33,7 @@
* 1100 - 1199 user space trusted application messages
* 1200 - 1299 messages internal to the audit daemon
* 1300 - 1399 audit event messages
* 1400 - 1499 SE Linux use
* 1400 - 1499 access control messages
* 1500 - 1599 kernel LSPP events
* 1600 - 1699 kernel crypto events
* 1700 - 1799 kernel anomaly records
@ -146,6 +146,8 @@
#define AUDIT_IPE_ACCESS 1420 /* IPE denial or grant */
#define AUDIT_IPE_CONFIG_CHANGE 1421 /* IPE config change */
#define AUDIT_IPE_POLICY_LOAD 1422 /* IPE policy load */
#define AUDIT_LANDLOCK_ACCESS 1423 /* Landlock denial */
#define AUDIT_LANDLOCK_DOMAIN 1424 /* Landlock domain status */
#define AUDIT_FIRST_KERN_ANOM_MSG 1700
#define AUDIT_LAST_KERN_ANOM_MSG 1799

View File

@ -4,6 +4,7 @@
*
* Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
* Copyright © 2021-2025 Microsoft Corporation
*/
#ifndef _UAPI_LINUX_LANDLOCK_H
@ -57,9 +58,43 @@ struct landlock_ruleset_attr {
*
* - %LANDLOCK_CREATE_RULESET_VERSION: Get the highest supported Landlock ABI
* version.
* - %LANDLOCK_CREATE_RULESET_ERRATA: Get a bitmask of fixed issues.
*/
/* clang-format off */
#define LANDLOCK_CREATE_RULESET_VERSION (1U << 0)
#define LANDLOCK_CREATE_RULESET_ERRATA (1U << 1)
/* clang-format on */
/*
* sys_landlock_restrict_self() flags:
*
* - %LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF: Do not create any log related to the
* enforced restrictions. This should only be set by tools launching unknown
* or untrusted programs (e.g. a sandbox tool, container runtime, system
* service manager). Because programs sandboxing themselves should fix any
* denied access, they should not set this flag to be aware of potential
* issues reported by system's logs (i.e. audit).
* - %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON: Explicitly ask to continue
* logging denied access requests even after an :manpage:`execve(2)` call.
* This flag should only be set if all the programs than can legitimately be
* executed will not try to request a denied access (which could spam audit
* logs).
* - %LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF: Do not create any log related
* to the enforced restrictions coming from future nested domains created by
* the caller or its descendants. This should only be set according to a
* runtime configuration (i.e. not hardcoded) by programs launching other
* unknown or untrusted programs that may create their own Landlock domains
* and spam logs. The main use case is for container runtimes to enable users
* to mute buggy sandboxed programs for a specific container image. Other use
* cases include sandboxer tools and init systems. Unlike
* %LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF,
* %LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF does not impact the requested
* restriction (if any) but only the future nested domains.
*/
/* clang-format off */
#define LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF (1U << 0)
#define LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON (1U << 1)
#define LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF (1U << 2)
/* clang-format on */
/**

View File

@ -58,6 +58,7 @@ static inline int landlock_restrict_self(const int ruleset_fd,
#define ENV_TCP_BIND_NAME "LL_TCP_BIND"
#define ENV_TCP_CONNECT_NAME "LL_TCP_CONNECT"
#define ENV_SCOPED_NAME "LL_SCOPED"
#define ENV_FORCE_LOG_NAME "LL_FORCE_LOG"
#define ENV_DELIMITER ":"
static int str2num(const char *numstr, __u64 *num_dst)
@ -295,7 +296,7 @@ out_unset:
/* clang-format on */
#define LANDLOCK_ABI_LAST 6
#define LANDLOCK_ABI_LAST 7
#define XSTR(s) #s
#define STR(s) XSTR(s)
@ -322,6 +323,9 @@ static const char help[] =
" - \"a\" to restrict opening abstract unix sockets\n"
" - \"s\" to restrict sending signals\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"
"\n"
"Example:\n"
ENV_FS_RO_NAME "=\"${PATH}:/lib:/usr:/proc:/etc:/dev/urandom\" "
ENV_FS_RW_NAME "=\"/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp\" "
@ -340,7 +344,7 @@ int main(const int argc, char *const argv[], char *const *const envp)
const char *cmd_path;
char *const *cmd_argv;
int ruleset_fd, abi;
char *env_port_name;
char *env_port_name, *env_force_log;
__u64 access_fs_ro = ACCESS_FS_ROUGHLY_READ,
access_fs_rw = ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE;
@ -351,6 +355,8 @@ int main(const int argc, char *const argv[], char *const *const envp)
.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
LANDLOCK_SCOPE_SIGNAL,
};
int supported_restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
int set_restrict_flags = 0;
if (argc < 2) {
fprintf(stderr, help, argv[0]);
@ -422,6 +428,13 @@ int main(const int argc, char *const argv[], char *const *const envp)
/* Removes LANDLOCK_SCOPE_* for ABI < 6 */
ruleset_attr.scoped &= ~(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
LANDLOCK_SCOPE_SIGNAL);
__attribute__((fallthrough));
case 6:
/* Removes LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON for ABI < 7 */
supported_restrict_flags &=
~LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
/* Must be printed for any ABI < LANDLOCK_ABI_LAST. */
fprintf(stderr,
"Hint: You should update the running kernel "
"to leverage Landlock features "
@ -456,6 +469,24 @@ int main(const int argc, char *const argv[], char *const *const envp)
if (check_ruleset_scope(ENV_SCOPED_NAME, &ruleset_attr))
return 1;
/* Enables optional logs. */
env_force_log = getenv(ENV_FORCE_LOG_NAME);
if (env_force_log) {
if (strcmp(env_force_log, "1") != 0) {
fprintf(stderr, "Unknown value for " ENV_FORCE_LOG_NAME
" (only \"1\" is handled)\n");
return 1;
}
if (!(supported_restrict_flags &
LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON)) {
fprintf(stderr,
"Audit logs not supported by current kernel\n");
return 1;
}
set_restrict_flags |= LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
unsetenv(ENV_FORCE_LOG_NAME);
}
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0) {
@ -483,7 +514,7 @@ int main(const int argc, char *const argv[], char *const *const envp)
perror("Failed to restrict privileges");
goto err_close_ruleset;
}
if (landlock_restrict_self(ruleset_fd, 0)) {
if (landlock_restrict_self(ruleset_fd, set_restrict_flags)) {
perror("Failed to enforce ruleset");
goto err_close_ruleset;
}

View File

@ -1,4 +1,6 @@
CONFIG_AUDIT=y
CONFIG_KUNIT=y
CONFIG_NET=y
CONFIG_SECURITY=y
CONFIG_SECURITY_LANDLOCK=y
CONFIG_SECURITY_LANDLOCK_KUNIT_TEST=y

View File

@ -4,3 +4,8 @@ landlock-y := setup.o syscalls.o object.o ruleset.o \
cred.o task.o fs.o
landlock-$(CONFIG_INET) += net.o
landlock-$(CONFIG_AUDIT) += \
id.o \
audit.o \
domain.o

View File

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Landlock LSM - Access types and helpers
* Landlock - Access types and helpers
*
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
@ -28,6 +28,12 @@
LANDLOCK_ACCESS_FS_REFER)
/* clang-format on */
/* clang-format off */
#define _LANDLOCK_ACCESS_FS_OPTIONAL ( \
LANDLOCK_ACCESS_FS_TRUNCATE | \
LANDLOCK_ACCESS_FS_IOCTL_DEV)
/* clang-format on */
typedef u16 access_mask_t;
/* Makes sure all filesystem access rights can be stored. */
@ -60,6 +66,23 @@ typedef u16 layer_mask_t;
/* Makes sure all layers can be checked. */
static_assert(BITS_PER_TYPE(layer_mask_t) >= LANDLOCK_MAX_NUM_LAYERS);
/*
* Tracks domains responsible of a denied access. This is required to avoid
* storing in each object the full layer_masks[] required by update_request().
*/
typedef u8 deny_masks_t;
/*
* Makes sure all optional access rights can be tied to a layer index (cf.
* get_deny_mask).
*/
static_assert(BITS_PER_TYPE(deny_masks_t) >=
(HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1) *
HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL)));
/* LANDLOCK_MAX_NUM_LAYERS must be a power of two (cf. deny_masks_t assert). */
static_assert(HWEIGHT(LANDLOCK_MAX_NUM_LAYERS) == 1);
/* Upgrades with all initially denied by default access rights. */
static inline struct access_masks
landlock_upgrade_handled_access_masks(struct access_masks access_masks)

522
security/landlock/audit.c Normal file
View File

@ -0,0 +1,522 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Landlock - Audit helpers
*
* Copyright © 2023-2025 Microsoft Corporation
*/
#include <kunit/test.h>
#include <linux/audit.h>
#include <linux/bitops.h>
#include <linux/lsm_audit.h>
#include <linux/pid.h>
#include <uapi/linux/landlock.h>
#include "access.h"
#include "audit.h"
#include "common.h"
#include "cred.h"
#include "domain.h"
#include "limits.h"
#include "ruleset.h"
static const char *const fs_access_strings[] = {
[BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = "fs.execute",
[BIT_INDEX(LANDLOCK_ACCESS_FS_WRITE_FILE)] = "fs.write_file",
[BIT_INDEX(LANDLOCK_ACCESS_FS_READ_FILE)] = "fs.read_file",
[BIT_INDEX(LANDLOCK_ACCESS_FS_READ_DIR)] = "fs.read_dir",
[BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_DIR)] = "fs.remove_dir",
[BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_FILE)] = "fs.remove_file",
[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_CHAR)] = "fs.make_char",
[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_DIR)] = "fs.make_dir",
[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_REG)] = "fs.make_reg",
[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SOCK)] = "fs.make_sock",
[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_FIFO)] = "fs.make_fifo",
[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_BLOCK)] = "fs.make_block",
[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SYM)] = "fs.make_sym",
[BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer",
[BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate",
[BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev",
};
static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);
static const char *const net_access_strings[] = {
[BIT_INDEX(LANDLOCK_ACCESS_NET_BIND_TCP)] = "net.bind_tcp",
[BIT_INDEX(LANDLOCK_ACCESS_NET_CONNECT_TCP)] = "net.connect_tcp",
};
static_assert(ARRAY_SIZE(net_access_strings) == LANDLOCK_NUM_ACCESS_NET);
static __attribute_const__ const char *
get_blocker(const enum landlock_request_type type,
const unsigned long access_bit)
{
switch (type) {
case LANDLOCK_REQUEST_PTRACE:
WARN_ON_ONCE(access_bit != -1);
return "ptrace";
case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY:
WARN_ON_ONCE(access_bit != -1);
return "fs.change_topology";
case LANDLOCK_REQUEST_FS_ACCESS:
if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(fs_access_strings)))
return "unknown";
return fs_access_strings[access_bit];
case LANDLOCK_REQUEST_NET_ACCESS:
if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(net_access_strings)))
return "unknown";
return net_access_strings[access_bit];
case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
WARN_ON_ONCE(access_bit != -1);
return "scope.abstract_unix_socket";
case LANDLOCK_REQUEST_SCOPE_SIGNAL:
WARN_ON_ONCE(access_bit != -1);
return "scope.signal";
}
WARN_ON_ONCE(1);
return "unknown";
}
static void log_blockers(struct audit_buffer *const ab,
const enum landlock_request_type type,
const access_mask_t access)
{
const unsigned long access_mask = access;
unsigned long access_bit;
bool is_first = true;
for_each_set_bit(access_bit, &access_mask, BITS_PER_TYPE(access)) {
audit_log_format(ab, "%s%s", is_first ? "" : ",",
get_blocker(type, access_bit));
is_first = false;
}
if (is_first)
audit_log_format(ab, "%s", get_blocker(type, -1));
}
static void log_domain(struct landlock_hierarchy *const hierarchy)
{
struct audit_buffer *ab;
/* Ignores already logged domains. */
if (READ_ONCE(hierarchy->log_status) == LANDLOCK_LOG_RECORDED)
return;
/* Uses consistent allocation flags wrt common_lsm_audit(). */
ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN,
AUDIT_LANDLOCK_DOMAIN);
if (!ab)
return;
WARN_ON_ONCE(hierarchy->id == 0);
audit_log_format(
ab,
"domain=%llx status=allocated mode=enforcing pid=%d uid=%u exe=",
hierarchy->id, pid_nr(hierarchy->details->pid),
hierarchy->details->uid);
audit_log_untrustedstring(ab, hierarchy->details->exe_path);
audit_log_format(ab, " comm=");
audit_log_untrustedstring(ab, hierarchy->details->comm);
audit_log_end(ab);
/*
* There may be race condition leading to logging of the same domain
* several times but that is OK.
*/
WRITE_ONCE(hierarchy->log_status, LANDLOCK_LOG_RECORDED);
}
static struct landlock_hierarchy *
get_hierarchy(const struct landlock_ruleset *const domain, const size_t layer)
{
struct landlock_hierarchy *hierarchy = domain->hierarchy;
ssize_t i;
if (WARN_ON_ONCE(layer >= domain->num_layers))
return hierarchy;
for (i = domain->num_layers - 1; i > layer; i--) {
if (WARN_ON_ONCE(!hierarchy->parent))
break;
hierarchy = hierarchy->parent;
}
return hierarchy;
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static void test_get_hierarchy(struct kunit *const test)
{
struct landlock_hierarchy dom0_hierarchy = {
.id = 10,
};
struct landlock_hierarchy dom1_hierarchy = {
.parent = &dom0_hierarchy,
.id = 20,
};
struct landlock_hierarchy dom2_hierarchy = {
.parent = &dom1_hierarchy,
.id = 30,
};
struct landlock_ruleset dom2 = {
.hierarchy = &dom2_hierarchy,
.num_layers = 3,
};
KUNIT_EXPECT_EQ(test, 10, get_hierarchy(&dom2, 0)->id);
KUNIT_EXPECT_EQ(test, 20, get_hierarchy(&dom2, 1)->id);
KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, 2)->id);
KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, -1)->id);
}
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
static size_t get_denied_layer(const struct landlock_ruleset *const domain,
access_mask_t *const access_request,
const layer_mask_t (*const layer_masks)[],
const size_t layer_masks_size)
{
const unsigned long access_req = *access_request;
unsigned long access_bit;
access_mask_t missing = 0;
long youngest_layer = -1;
for_each_set_bit(access_bit, &access_req, layer_masks_size) {
const access_mask_t mask = (*layer_masks)[access_bit];
long layer;
if (!mask)
continue;
/* __fls(1) == 0 */
layer = __fls(mask);
if (layer > youngest_layer) {
youngest_layer = layer;
missing = BIT(access_bit);
} else if (layer == youngest_layer) {
missing |= BIT(access_bit);
}
}
*access_request = missing;
if (youngest_layer == -1)
return domain->num_layers - 1;
return youngest_layer;
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static void test_get_denied_layer(struct kunit *const test)
{
const struct landlock_ruleset dom = {
.num_layers = 5,
};
const layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {
[BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT(0),
[BIT_INDEX(LANDLOCK_ACCESS_FS_READ_FILE)] = BIT(1),
[BIT_INDEX(LANDLOCK_ACCESS_FS_READ_DIR)] = BIT(1) | BIT(0),
[BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_DIR)] = BIT(2),
};
access_mask_t access;
access = LANDLOCK_ACCESS_FS_EXECUTE;
KUNIT_EXPECT_EQ(test, 0,
get_denied_layer(&dom, &access, &layer_masks,
sizeof(layer_masks)));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_EXECUTE);
access = LANDLOCK_ACCESS_FS_READ_FILE;
KUNIT_EXPECT_EQ(test, 1,
get_denied_layer(&dom, &access, &layer_masks,
sizeof(layer_masks)));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_FILE);
access = LANDLOCK_ACCESS_FS_READ_DIR;
KUNIT_EXPECT_EQ(test, 1,
get_denied_layer(&dom, &access, &layer_masks,
sizeof(layer_masks)));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR);
access = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR;
KUNIT_EXPECT_EQ(test, 1,
get_denied_layer(&dom, &access, &layer_masks,
sizeof(layer_masks)));
KUNIT_EXPECT_EQ(test, access,
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR);
access = LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_DIR;
KUNIT_EXPECT_EQ(test, 1,
get_denied_layer(&dom, &access, &layer_masks,
sizeof(layer_masks)));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR);
access = LANDLOCK_ACCESS_FS_WRITE_FILE;
KUNIT_EXPECT_EQ(test, 4,
get_denied_layer(&dom, &access, &layer_masks,
sizeof(layer_masks)));
KUNIT_EXPECT_EQ(test, access, 0);
}
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
static size_t
get_layer_from_deny_masks(access_mask_t *const access_request,
const access_mask_t all_existing_optional_access,
const deny_masks_t deny_masks)
{
const unsigned long access_opt = all_existing_optional_access;
const unsigned long access_req = *access_request;
access_mask_t missing = 0;
size_t youngest_layer = 0;
size_t access_index = 0;
unsigned long access_bit;
/* This will require change with new object types. */
WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL);
for_each_set_bit(access_bit, &access_opt,
BITS_PER_TYPE(access_mask_t)) {
if (access_req & BIT(access_bit)) {
const size_t layer =
(deny_masks >> (access_index * 4)) &
(LANDLOCK_MAX_NUM_LAYERS - 1);
if (layer > youngest_layer) {
youngest_layer = layer;
missing = BIT(access_bit);
} else if (layer == youngest_layer) {
missing |= BIT(access_bit);
}
}
access_index++;
}
*access_request = missing;
return youngest_layer;
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static void test_get_layer_from_deny_masks(struct kunit *const test)
{
deny_masks_t deny_mask;
access_mask_t access;
/* truncate:0 ioctl_dev:2 */
deny_mask = 0x20;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 0,
get_layer_from_deny_masks(&access,
_LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(&access,
_LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
/* truncate:15 ioctl_dev:15 */
deny_mask = 0xff;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 15,
get_layer_from_deny_masks(&access,
_LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 15,
get_layer_from_deny_masks(&access,
_LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask));
KUNIT_EXPECT_EQ(test, access,
LANDLOCK_ACCESS_FS_TRUNCATE |
LANDLOCK_ACCESS_FS_IOCTL_DEV);
}
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
static bool is_valid_request(const struct landlock_request *const request)
{
if (WARN_ON_ONCE(request->layer_plus_one > LANDLOCK_MAX_NUM_LAYERS))
return false;
if (WARN_ON_ONCE(!(!!request->layer_plus_one ^ !!request->access)))
return false;
if (request->access) {
if (WARN_ON_ONCE(!(!!request->layer_masks ^
!!request->all_existing_optional_access)))
return false;
} else {
if (WARN_ON_ONCE(request->layer_masks ||
request->all_existing_optional_access))
return false;
}
if (WARN_ON_ONCE(!!request->layer_masks ^ !!request->layer_masks_size))
return false;
if (request->deny_masks) {
if (WARN_ON_ONCE(!request->all_existing_optional_access))
return false;
}
return true;
}
/**
* landlock_log_denial - Create audit records related to a denial
*
* @subject: The Landlock subject's credential denying an action.
* @request: Detail of the user space request.
*/
void landlock_log_denial(const struct landlock_cred_security *const subject,
const struct landlock_request *const request)
{
struct audit_buffer *ab;
struct landlock_hierarchy *youngest_denied;
size_t youngest_layer;
access_mask_t missing;
if (WARN_ON_ONCE(!subject || !subject->domain ||
!subject->domain->hierarchy || !request))
return;
if (!is_valid_request(request))
return;
missing = request->access;
if (missing) {
/* Gets the nearest domain that denies the request. */
if (request->layer_masks) {
youngest_layer = get_denied_layer(
subject->domain, &missing, request->layer_masks,
request->layer_masks_size);
} else {
youngest_layer = get_layer_from_deny_masks(
&missing, request->all_existing_optional_access,
request->deny_masks);
}
youngest_denied =
get_hierarchy(subject->domain, youngest_layer);
} else {
youngest_layer = request->layer_plus_one - 1;
youngest_denied =
get_hierarchy(subject->domain, youngest_layer);
}
if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED)
return;
/*
* Consistently keeps track of the number of denied access requests
* even if audit is currently disabled, or if audit rules currently
* exclude this record type, or if landlock_restrict_self(2)'s flags
* quiet logs.
*/
atomic64_inc(&youngest_denied->num_denials);
if (!audit_enabled)
return;
/* Checks if the current exec was restricting itself. */
if (subject->domain_exec & (1 << youngest_layer)) {
/* Ignores denials for the same execution. */
if (!youngest_denied->log_same_exec)
return;
} else {
/* Ignores denials after a new execution. */
if (!youngest_denied->log_new_exec)
return;
}
/* Uses consistent allocation flags wrt common_lsm_audit(). */
ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN,
AUDIT_LANDLOCK_ACCESS);
if (!ab)
return;
audit_log_format(ab, "domain=%llx blockers=", youngest_denied->id);
log_blockers(ab, request->type, missing);
audit_log_lsm_data(ab, &request->audit);
audit_log_end(ab);
/* Logs this domain the first time it shows in log. */
log_domain(youngest_denied);
}
/**
* landlock_log_drop_domain - Create an audit record on domain deallocation
*
* @hierarchy: The domain's hierarchy being deallocated.
*
* Only domains which previously appeared in the audit logs are logged again.
* This is useful to know when a domain will never show again in the audit log.
*
* Called in a work queue scheduled by landlock_put_ruleset_deferred() called
* by hook_cred_free().
*/
void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
{
struct audit_buffer *ab;
if (WARN_ON_ONCE(!hierarchy))
return;
if (!audit_enabled)
return;
/* Ignores domains that were not logged. */
if (READ_ONCE(hierarchy->log_status) != LANDLOCK_LOG_RECORDED)
return;
/*
* If logging of domain allocation succeeded, warns about failure to log
* domain deallocation to highlight unbalanced domain lifetime logs.
*/
ab = audit_log_start(audit_context(), GFP_KERNEL,
AUDIT_LANDLOCK_DOMAIN);
if (!ab)
return;
audit_log_format(ab, "domain=%llx status=deallocated denials=%llu",
hierarchy->id, atomic64_read(&hierarchy->num_denials));
audit_log_end(ab);
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static struct kunit_case test_cases[] = {
/* clang-format off */
KUNIT_CASE(test_get_hierarchy),
KUNIT_CASE(test_get_denied_layer),
KUNIT_CASE(test_get_layer_from_deny_masks),
{}
/* clang-format on */
};
static struct kunit_suite test_suite = {
.name = "landlock_audit",
.test_cases = test_cases,
};
kunit_test_suite(test_suite);
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */

76
security/landlock/audit.h Normal file
View File

@ -0,0 +1,76 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Landlock - Audit helpers
*
* Copyright © 2023-2025 Microsoft Corporation
*/
#ifndef _SECURITY_LANDLOCK_AUDIT_H
#define _SECURITY_LANDLOCK_AUDIT_H
#include <linux/audit.h>
#include <linux/lsm_audit.h>
#include "access.h"
#include "cred.h"
enum landlock_request_type {
LANDLOCK_REQUEST_PTRACE = 1,
LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY,
LANDLOCK_REQUEST_FS_ACCESS,
LANDLOCK_REQUEST_NET_ACCESS,
LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
LANDLOCK_REQUEST_SCOPE_SIGNAL,
};
/*
* We should be careful to only use a variable of this type for
* landlock_log_denial(). This way, the compiler can remove it entirely if
* CONFIG_AUDIT is not set.
*/
struct landlock_request {
/* Mandatory fields. */
enum landlock_request_type type;
struct common_audit_data audit;
/**
* layer_plus_one: First layer level that denies the request + 1. The
* extra one is useful to detect uninitialized field.
*/
size_t layer_plus_one;
/* Required field for configurable access control. */
access_mask_t access;
/* Required fields for requests with layer masks. */
const layer_mask_t (*layer_masks)[];
size_t layer_masks_size;
/* Required fields for requests with deny masks. */
const access_mask_t all_existing_optional_access;
deny_masks_t deny_masks;
};
#ifdef CONFIG_AUDIT
void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy);
void landlock_log_denial(const struct landlock_cred_security *const subject,
const struct landlock_request *const request);
#else /* CONFIG_AUDIT */
static inline void
landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
{
}
static inline void
landlock_log_denial(const struct landlock_cred_security *const subject,
const struct landlock_request *const request)
{
}
#endif /* CONFIG_AUDIT */
#endif /* _SECURITY_LANDLOCK_AUDIT_H */

View File

@ -1,11 +1,13 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Landlock LSM - Credential hooks
* Landlock - Credential hooks
*
* Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
* Copyright © 2024-2025 Microsoft Corporation
*/
#include <linux/binfmts.h>
#include <linux/cred.h>
#include <linux/lsm_hooks.h>
@ -17,11 +19,12 @@
static void hook_cred_transfer(struct cred *const new,
const struct cred *const old)
{
struct landlock_ruleset *const old_dom = landlock_cred(old)->domain;
const struct landlock_cred_security *const old_llcred =
landlock_cred(old);
if (old_dom) {
landlock_get_ruleset(old_dom);
landlock_cred(new)->domain = old_dom;
if (old_llcred->domain) {
landlock_get_ruleset(old_llcred->domain);
*landlock_cred(new) = *old_llcred;
}
}
@ -40,10 +43,25 @@ static void hook_cred_free(struct cred *const cred)
landlock_put_ruleset_deferred(dom);
}
#ifdef CONFIG_AUDIT
static int hook_bprm_creds_for_exec(struct linux_binprm *const bprm)
{
/* Resets for each execution. */
landlock_cred(bprm->cred)->domain_exec = 0;
return 0;
}
#endif /* CONFIG_AUDIT */
static struct security_hook_list landlock_hooks[] __ro_after_init = {
LSM_HOOK_INIT(cred_prepare, hook_cred_prepare),
LSM_HOOK_INIT(cred_transfer, hook_cred_transfer),
LSM_HOOK_INIT(cred_free, hook_cred_free),
#ifdef CONFIG_AUDIT
LSM_HOOK_INIT(bprm_creds_for_exec, hook_bprm_creds_for_exec),
#endif /* CONFIG_AUDIT */
};
__init void landlock_add_cred_hooks(void)

View File

@ -1,24 +1,63 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Landlock LSM - Credential hooks
* Landlock - Credential hooks
*
* Copyright © 2019-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2019-2020 ANSSI
* Copyright © 2021-2025 Microsoft Corporation
*/
#ifndef _SECURITY_LANDLOCK_CRED_H
#define _SECURITY_LANDLOCK_CRED_H
#include <linux/container_of.h>
#include <linux/cred.h>
#include <linux/init.h>
#include <linux/rcupdate.h>
#include "access.h"
#include "limits.h"
#include "ruleset.h"
#include "setup.h"
/**
* struct landlock_cred_security - Credential security blob
*
* This structure is packed to minimize the size of struct
* landlock_file_security. However, it is always aligned in the LSM cred blob,
* see lsm_set_blob_size().
*/
struct landlock_cred_security {
/**
* @domain: Immutable ruleset enforced on a task.
*/
struct landlock_ruleset *domain;
};
#ifdef CONFIG_AUDIT
/**
* @domain_exec: Bitmask identifying the domain layers that were enforced by
* the current task's executed file (i.e. no new execve(2) since
* landlock_restrict_self(2)).
*/
u16 domain_exec;
/**
* @log_subdomains_off: Set if the domain descendants's log_status should be
* set to %LANDLOCK_LOG_DISABLED. This is not a landlock_hierarchy
* configuration because it applies to future descendant domains and it does
* not require a current domain.
*/
u8 log_subdomains_off : 1;
#endif /* CONFIG_AUDIT */
} __packed;
#ifdef CONFIG_AUDIT
/* Makes sure all layer executions can be stored. */
static_assert(BITS_PER_TYPE(typeof_member(struct landlock_cred_security,
domain_exec)) >=
LANDLOCK_MAX_NUM_LAYERS);
#endif /* CONFIG_AUDIT */
static inline struct landlock_cred_security *
landlock_cred(const struct cred *cred)
@ -53,6 +92,55 @@ static inline bool landlocked(const struct task_struct *const task)
return has_dom;
}
/**
* landlock_get_applicable_subject - Return the subject's Landlock credential
* if its enforced domain applies to (i.e.
* handles) at least one of the access rights
* specified in @masks
*
* @cred: credential
* @masks: access masks
* @handle_layer: returned youngest layer handling a subset of @masks. Not set
* if the function returns NULL.
*
* Returns: landlock_cred(@cred) if any access rights specified in @masks is
* handled, or NULL otherwise.
*/
static inline const struct landlock_cred_security *
landlock_get_applicable_subject(const struct cred *const cred,
const struct access_masks masks,
size_t *const handle_layer)
{
const union access_masks_all masks_all = {
.masks = masks,
};
const struct landlock_ruleset *domain;
ssize_t layer_level;
if (!cred)
return NULL;
domain = landlock_cred(cred)->domain;
if (!domain)
return NULL;
for (layer_level = domain->num_layers - 1; layer_level >= 0;
layer_level--) {
union access_masks_all layer = {
.masks = domain->access_masks[layer_level],
};
if (layer.all & masks_all.all) {
if (handle_layer)
*handle_layer = layer_level;
return landlock_cred(cred);
}
}
return NULL;
}
__init void landlock_add_cred_hooks(void);
#endif /* _SECURITY_LANDLOCK_CRED_H */

264
security/landlock/domain.c Normal file
View File

@ -0,0 +1,264 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Landlock - Domain management
*
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
* Copyright © 2024-2025 Microsoft Corporation
*/
#include <kunit/test.h>
#include <linux/bitops.h>
#include <linux/bits.h>
#include <linux/cred.h>
#include <linux/file.h>
#include <linux/mm.h>
#include <linux/path.h>
#include <linux/pid.h>
#include <linux/sched.h>
#include <linux/uidgid.h>
#include "access.h"
#include "common.h"
#include "domain.h"
#include "id.h"
#ifdef CONFIG_AUDIT
/**
* get_current_exe - Get the current's executable path, if any
*
* @exe_str: Returned pointer to a path string with a lifetime tied to the
* returned buffer, if any.
* @exe_size: Returned size of @exe_str (including the trailing null
* character), if any.
*
* Returns: A pointer to an allocated buffer where @exe_str point to, %NULL if
* there is no executable path, or an error otherwise.
*/
static const void *get_current_exe(const char **const exe_str,
size_t *const exe_size)
{
const size_t buffer_size = LANDLOCK_PATH_MAX_SIZE;
struct mm_struct *mm = current->mm;
struct file *file __free(fput) = NULL;
char *buffer __free(kfree) = NULL;
const char *exe;
ssize_t size;
if (!mm)
return NULL;
file = get_mm_exe_file(mm);
if (!file)
return NULL;
buffer = kmalloc(buffer_size, GFP_KERNEL);
if (!buffer)
return ERR_PTR(-ENOMEM);
exe = d_path(&file->f_path, buffer, buffer_size);
if (WARN_ON_ONCE(IS_ERR(exe)))
/* Should never happen according to LANDLOCK_PATH_MAX_SIZE. */
return ERR_CAST(exe);
size = buffer + buffer_size - exe;
if (WARN_ON_ONCE(size <= 0))
return ERR_PTR(-ENAMETOOLONG);
*exe_size = size;
*exe_str = exe;
return no_free_ptr(buffer);
}
/*
* Returns: A newly allocated object describing a domain, or an error
* otherwise.
*/
static struct landlock_details *get_current_details(void)
{
/* Cf. audit_log_d_path_exe() */
static const char null_path[] = "(null)";
const char *path_str = null_path;
size_t path_size = sizeof(null_path);
const void *buffer __free(kfree) = NULL;
struct landlock_details *details;
buffer = get_current_exe(&path_str, &path_size);
if (IS_ERR(buffer))
return ERR_CAST(buffer);
/*
* Create the new details according to the path's length. Do not
* allocate with GFP_KERNEL_ACCOUNT because it is independent from the
* caller.
*/
details =
kzalloc(struct_size(details, exe_path, path_size), GFP_KERNEL);
if (!details)
return ERR_PTR(-ENOMEM);
memcpy(details->exe_path, path_str, path_size);
WARN_ON_ONCE(current_cred() != current_real_cred());
details->pid = get_pid(task_pid(current));
details->uid = from_kuid(&init_user_ns, current_uid());
get_task_comm(details->comm, current);
return details;
}
/**
* landlock_init_hierarchy_log - Partially initialize landlock_hierarchy
*
* @hierarchy: The hierarchy to initialize.
*
* The current task is referenced as the domain that is enforcing the
* restriction. The subjective credentials must not be in an overridden state.
*
* @hierarchy->parent and @hierarchy->usage should already be set.
*/
int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)
{
struct landlock_details *details;
details = get_current_details();
if (IS_ERR(details))
return PTR_ERR(details);
hierarchy->details = details;
hierarchy->id = landlock_get_id_range(1);
hierarchy->log_status = LANDLOCK_LOG_PENDING;
hierarchy->log_same_exec = true;
hierarchy->log_new_exec = false;
atomic64_set(&hierarchy->num_denials, 0);
return 0;
}
static deny_masks_t
get_layer_deny_mask(const access_mask_t all_existing_optional_access,
const unsigned long access_bit, const size_t layer)
{
unsigned long access_weight;
/* This may require change with new object types. */
WARN_ON_ONCE(all_existing_optional_access !=
_LANDLOCK_ACCESS_FS_OPTIONAL);
if (WARN_ON_ONCE(layer >= LANDLOCK_MAX_NUM_LAYERS))
return 0;
access_weight = hweight_long(all_existing_optional_access &
GENMASK(access_bit, 0));
if (WARN_ON_ONCE(access_weight < 1))
return 0;
return layer
<< ((access_weight - 1) * HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1));
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static void test_get_layer_deny_mask(struct kunit *const test)
{
const unsigned long truncate = BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE);
const unsigned long ioctl_dev = BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, 0,
get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL,
truncate, 0));
KUNIT_EXPECT_EQ(test, 0x3,
get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL,
truncate, 3));
KUNIT_EXPECT_EQ(test, 0,
get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL,
ioctl_dev, 0));
KUNIT_EXPECT_EQ(test, 0xf0,
get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL,
ioctl_dev, 15));
}
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
deny_masks_t
landlock_get_deny_masks(const access_mask_t all_existing_optional_access,
const access_mask_t optional_access,
const layer_mask_t (*const layer_masks)[],
const size_t layer_masks_size)
{
const unsigned long access_opt = optional_access;
unsigned long access_bit;
deny_masks_t deny_masks = 0;
/* This may require change with new object types. */
WARN_ON_ONCE(access_opt !=
(optional_access & all_existing_optional_access));
if (WARN_ON_ONCE(!layer_masks))
return 0;
if (WARN_ON_ONCE(!access_opt))
return 0;
for_each_set_bit(access_bit, &access_opt, layer_masks_size) {
const layer_mask_t mask = (*layer_masks)[access_bit];
if (!mask)
continue;
/* __fls(1) == 0 */
deny_masks |= get_layer_deny_mask(all_existing_optional_access,
access_bit, __fls(mask));
}
return deny_masks;
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static void test_landlock_get_deny_masks(struct kunit *const test)
{
const layer_mask_t layers1[BITS_PER_TYPE(access_mask_t)] = {
[BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT_ULL(0) |
BIT_ULL(9),
[BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = BIT_ULL(1),
[BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = BIT_ULL(2) |
BIT_ULL(0),
};
KUNIT_EXPECT_EQ(test, 0x1,
landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL,
LANDLOCK_ACCESS_FS_TRUNCATE,
&layers1, ARRAY_SIZE(layers1)));
KUNIT_EXPECT_EQ(test, 0x20,
landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL,
LANDLOCK_ACCESS_FS_IOCTL_DEV,
&layers1, ARRAY_SIZE(layers1)));
KUNIT_EXPECT_EQ(
test, 0x21,
landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL,
LANDLOCK_ACCESS_FS_TRUNCATE |
LANDLOCK_ACCESS_FS_IOCTL_DEV,
&layers1, ARRAY_SIZE(layers1)));
}
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static struct kunit_case test_cases[] = {
/* clang-format off */
KUNIT_CASE(test_get_layer_deny_mask),
KUNIT_CASE(test_landlock_get_deny_masks),
{}
/* clang-format on */
};
static struct kunit_suite test_suite = {
.name = "landlock_domain",
.test_cases = test_cases,
};
kunit_test_suite(test_suite);
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
#endif /* CONFIG_AUDIT */

174
security/landlock/domain.h Normal file
View File

@ -0,0 +1,174 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Landlock - Domain management
*
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
* Copyright © 2024-2025 Microsoft Corporation
*/
#ifndef _SECURITY_LANDLOCK_DOMAIN_H
#define _SECURITY_LANDLOCK_DOMAIN_H
#include <linux/limits.h>
#include <linux/mm.h>
#include <linux/path.h>
#include <linux/pid.h>
#include <linux/refcount.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include "access.h"
#include "audit.h"
enum landlock_log_status {
LANDLOCK_LOG_PENDING = 0,
LANDLOCK_LOG_RECORDED,
LANDLOCK_LOG_DISABLED,
};
/**
* struct landlock_details - Domain's creation information
*
* Rarely accessed, mainly when logging the first domain's denial.
*
* The contained pointers are initialized at the domain creation time and never
* changed again. Contrary to most other Landlock object types, this one is
* not allocated with GFP_KERNEL_ACCOUNT because its size may not be under the
* caller's control (e.g. unknown exe_path) and the data is not explicitly
* requested nor used by tasks.
*/
struct landlock_details {
/**
* @pid: PID of the task that initially restricted itself. It still
* identifies the same task. Keeping a reference to this PID ensures that
* it will not be recycled.
*/
struct pid *pid;
/**
* @uid: UID of the task that initially restricted itself, at creation time.
*/
uid_t uid;
/**
* @comm: Command line of the task that initially restricted itself, at
* creation time. Always NULL terminated.
*/
char comm[TASK_COMM_LEN];
/**
* @exe_path: Executable path of the task that initially restricted
* itself, at creation time. Always NULL terminated, and never greater
* than LANDLOCK_PATH_MAX_SIZE.
*/
char exe_path[];
};
/* Adds 11 extra characters for the potential " (deleted)" suffix. */
#define LANDLOCK_PATH_MAX_SIZE (PATH_MAX + 11)
/* Makes sure the greatest landlock_details can be allocated. */
static_assert(struct_size_t(struct landlock_details, exe_path,
LANDLOCK_PATH_MAX_SIZE) <= KMALLOC_MAX_SIZE);
/**
* struct landlock_hierarchy - Node in a domain hierarchy
*/
struct landlock_hierarchy {
/**
* @parent: Pointer to the parent node, or NULL if it is a root
* Landlock domain.
*/
struct landlock_hierarchy *parent;
/**
* @usage: Number of potential children domains plus their parent
* domain.
*/
refcount_t usage;
#ifdef CONFIG_AUDIT
/**
* @log_status: Whether this domain should be logged or not. Because
* concurrent log entries may be created at the same time, it is still
* possible to have several domain records of the same domain.
*/
enum landlock_log_status log_status;
/**
* @num_denials: Number of access requests denied by this domain.
* Masked (i.e. never logged) denials are still counted.
*/
atomic64_t num_denials;
/**
* @id: Landlock domain ID, sets once at domain creation time.
*/
u64 id;
/**
* @details: Information about the related domain.
*/
const struct landlock_details *details;
/**
* @log_same_exec: Set if the domain is *not* configured with
* %LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF. Set to true by default.
*/
u32 log_same_exec : 1,
/**
* @log_new_exec: Set if the domain is configured with
* %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON. Set to false by default.
*/
log_new_exec : 1;
#endif /* CONFIG_AUDIT */
};
#ifdef CONFIG_AUDIT
deny_masks_t
landlock_get_deny_masks(const access_mask_t all_existing_optional_access,
const access_mask_t optional_access,
const layer_mask_t (*const layer_masks)[],
size_t layer_masks_size);
int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy);
static inline void
landlock_free_hierarchy_details(struct landlock_hierarchy *const hierarchy)
{
if (WARN_ON_ONCE(!hierarchy || !hierarchy->details))
return;
put_pid(hierarchy->details->pid);
kfree(hierarchy->details);
}
#else /* CONFIG_AUDIT */
static inline int
landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)
{
return 0;
}
static inline void
landlock_free_hierarchy_details(struct landlock_hierarchy *const hierarchy)
{
}
#endif /* CONFIG_AUDIT */
static inline void
landlock_get_hierarchy(struct landlock_hierarchy *const hierarchy)
{
if (hierarchy)
refcount_inc(&hierarchy->usage);
}
static inline void landlock_put_hierarchy(struct landlock_hierarchy *hierarchy)
{
while (hierarchy && refcount_dec_and_test(&hierarchy->usage)) {
const struct landlock_hierarchy *const freeme = hierarchy;
landlock_log_drop_domain(hierarchy);
landlock_free_hierarchy_details(hierarchy);
hierarchy = hierarchy->parent;
kfree(freeme);
}
}
#endif /* _SECURITY_LANDLOCK_DOMAIN_H */

View File

@ -0,0 +1,99 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Landlock - Errata information
*
* Copyright © 2025 Microsoft Corporation
*/
#ifndef _SECURITY_LANDLOCK_ERRATA_H
#define _SECURITY_LANDLOCK_ERRATA_H
#include <linux/init.h>
struct landlock_erratum {
const int abi;
const u8 number;
};
/* clang-format off */
#define LANDLOCK_ERRATUM(NUMBER) \
{ \
.abi = LANDLOCK_ERRATA_ABI, \
.number = NUMBER, \
},
/* clang-format on */
/*
* Some fixes may require user space to check if they are applied on the running
* kernel before using a specific feature. For instance, this applies when a
* restriction was previously too restrictive and is now getting relaxed (for
* compatibility or semantic reasons). However, non-visible changes for
* legitimate use (e.g. security fixes) do not require an erratum.
*/
static const struct landlock_erratum landlock_errata_init[] __initconst = {
/*
* Only Sparse may not implement __has_include. If a compiler does not
* implement __has_include, a warning will be printed at boot time (see
* setup.c).
*/
#ifdef __has_include
#define LANDLOCK_ERRATA_ABI 1
#if __has_include("errata/abi-1.h")
#include "errata/abi-1.h"
#endif
#undef LANDLOCK_ERRATA_ABI
#define LANDLOCK_ERRATA_ABI 2
#if __has_include("errata/abi-2.h")
#include "errata/abi-2.h"
#endif
#undef LANDLOCK_ERRATA_ABI
#define LANDLOCK_ERRATA_ABI 3
#if __has_include("errata/abi-3.h")
#include "errata/abi-3.h"
#endif
#undef LANDLOCK_ERRATA_ABI
#define LANDLOCK_ERRATA_ABI 4
#if __has_include("errata/abi-4.h")
#include "errata/abi-4.h"
#endif
#undef LANDLOCK_ERRATA_ABI
#define LANDLOCK_ERRATA_ABI 5
#if __has_include("errata/abi-5.h")
#include "errata/abi-5.h"
#endif
#undef LANDLOCK_ERRATA_ABI
#define LANDLOCK_ERRATA_ABI 6
#if __has_include("errata/abi-6.h")
#include "errata/abi-6.h"
#endif
#undef LANDLOCK_ERRATA_ABI
/*
* For each new erratum, we need to include all the ABI files up to the impacted
* ABI to make all potential future intermediate errata easy to backport.
*
* If such change involves more than one ABI addition, then it must be in a
* dedicated commit with the same Fixes tag as used for the actual fix.
*
* Each commit creating a new security/landlock/errata/abi-*.h file must have a
* Depends-on tag to reference the commit that previously added the line to
* include this new file, except if the original Fixes tag is enough.
*
* Each erratum must be documented in its related ABI file, and a dedicated
* commit must update Documentation/userspace-api/landlock.rst to include this
* erratum. This commit will not be backported.
*/
#endif
{}
};
#endif /* _SECURITY_LANDLOCK_ERRATA_H */

View File

@ -0,0 +1,15 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/**
* DOC: erratum_1
*
* Erratum 1: TCP socket identification
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* This fix addresses an issue where IPv4 and IPv6 stream sockets (e.g., SMC,
* MPTCP, or SCTP) were incorrectly restricted by TCP access rights during
* :manpage:`bind(2)` and :manpage:`connect(2)` operations. This change ensures
* that only TCP sockets are subject to TCP access rights, allowing other
* protocols to operate without unnecessary restrictions.
*/
LANDLOCK_ERRATUM(1)

View File

@ -0,0 +1,19 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/**
* DOC: erratum_2
*
* Erratum 2: Scoped signal handling
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* This fix addresses an issue where signal scoping was overly restrictive,
* preventing sandboxed threads from signaling other threads within the same
* process if they belonged to different domains. Because threads are not
* security boundaries, user space might assume that any thread within the same
* process can send signals between themselves (see :manpage:`nptl(7)` and
* :manpage:`libpsx(3)`). Consistent with :manpage:`ptrace(2)` behavior, direct
* interaction between threads of the same process should always be allowed.
* This change ensures that any thread is allowed to send signals to any other
* thread within the same process, regardless of their domain.
*/
LANDLOCK_ERRATUM(2)

View File

@ -1,10 +1,10 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Landlock LSM - Filesystem management and hooks
* Landlock - Filesystem management and hooks
*
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
* Copyright © 2021-2022 Microsoft Corporation
* Copyright © 2021-2025 Microsoft Corporation
* Copyright © 2022 Günther Noack <gnoack3000@gmail.com>
* Copyright © 2023-2024 Google LLC
*/
@ -23,11 +23,14 @@
#include <linux/kernel.h>
#include <linux/limits.h>
#include <linux/list.h>
#include <linux/lsm_audit.h>
#include <linux/lsm_hooks.h>
#include <linux/mount.h>
#include <linux/namei.h>
#include <linux/path.h>
#include <linux/pid.h>
#include <linux/rcupdate.h>
#include <linux/sched/signal.h>
#include <linux/spinlock.h>
#include <linux/stat.h>
#include <linux/types.h>
@ -37,8 +40,10 @@
#include <uapi/linux/landlock.h>
#include "access.h"
#include "audit.h"
#include "common.h"
#include "cred.h"
#include "domain.h"
#include "fs.h"
#include "limits.h"
#include "object.h"
@ -393,12 +398,6 @@ static const struct access_masks any_fs = {
.fs = ~0,
};
static const struct landlock_ruleset *get_current_fs_domain(void)
{
return landlock_get_applicable_domain(landlock_get_current_domain(),
any_fs);
}
/*
* Check that a destination file hierarchy has more restrictions than a source
* file hierarchy. This is only used for link and rename actions.
@ -728,6 +727,7 @@ static void test_is_eacces_with_write(struct kunit *const test)
* those identified by @access_request_parent1). This matrix can
* initially refer to domain layer masks and, when the accesses for the
* destination and source are the same, to requested layer masks.
* @log_request_parent1: Audit request to fill if the related access is denied.
* @dentry_child1: Dentry to the initial child of the parent1 path. This
* pointer must be NULL for non-refer actions (i.e. not link nor rename).
* @access_request_parent2: Similar to @access_request_parent1 but for a
@ -736,6 +736,7 @@ static void test_is_eacces_with_write(struct kunit *const test)
* the source. Must be set to 0 when using a simple path request.
* @layer_masks_parent2: Similar to @layer_masks_parent1 but for a refer
* action. This must be NULL otherwise.
* @log_request_parent2: Audit request to fill if the related access is denied.
* @dentry_child2: Dentry to the initial child of the parent2 path. This
* pointer is only set for RENAME_EXCHANGE actions and must be NULL
* otherwise.
@ -755,10 +756,12 @@ static bool is_access_to_paths_allowed(
const struct path *const path,
const access_mask_t access_request_parent1,
layer_mask_t (*const layer_masks_parent1)[LANDLOCK_NUM_ACCESS_FS],
const struct dentry *const dentry_child1,
struct landlock_request *const log_request_parent1,
struct dentry *const dentry_child1,
const access_mask_t access_request_parent2,
layer_mask_t (*const layer_masks_parent2)[LANDLOCK_NUM_ACCESS_FS],
const struct dentry *const dentry_child2)
struct landlock_request *const log_request_parent2,
struct dentry *const dentry_child2)
{
bool allowed_parent1 = false, allowed_parent2 = false, is_dom_check,
child1_is_directory = true, child2_is_directory = true;
@ -771,11 +774,14 @@ static bool is_access_to_paths_allowed(
if (!access_request_parent1 && !access_request_parent2)
return true;
if (WARN_ON_ONCE(!domain || !path))
if (WARN_ON_ONCE(!path))
return true;
if (is_nouser_or_private(path->dentry))
return true;
if (WARN_ON_ONCE(domain->num_layers < 1 || !layer_masks_parent1))
if (WARN_ON_ONCE(!layer_masks_parent1))
return false;
allowed_parent1 = is_layer_masks_allowed(layer_masks_parent1);
@ -920,24 +926,51 @@ jump_up:
}
path_put(&walker_path);
if (!allowed_parent1) {
log_request_parent1->type = LANDLOCK_REQUEST_FS_ACCESS;
log_request_parent1->audit.type = LSM_AUDIT_DATA_PATH;
log_request_parent1->audit.u.path = *path;
log_request_parent1->access = access_masked_parent1;
log_request_parent1->layer_masks = layer_masks_parent1;
log_request_parent1->layer_masks_size =
ARRAY_SIZE(*layer_masks_parent1);
}
if (!allowed_parent2) {
log_request_parent2->type = LANDLOCK_REQUEST_FS_ACCESS;
log_request_parent2->audit.type = LSM_AUDIT_DATA_PATH;
log_request_parent2->audit.u.path = *path;
log_request_parent2->access = access_masked_parent2;
log_request_parent2->layer_masks = layer_masks_parent2;
log_request_parent2->layer_masks_size =
ARRAY_SIZE(*layer_masks_parent2);
}
return allowed_parent1 && allowed_parent2;
}
static int current_check_access_path(const struct path *const path,
access_mask_t access_request)
{
const struct landlock_ruleset *const dom = get_current_fs_domain();
const struct access_masks masks = {
.fs = access_request,
};
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), masks, NULL);
layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
struct landlock_request request = {};
if (!dom)
if (!subject)
return 0;
access_request = landlock_init_layer_masks(
dom, access_request, &layer_masks, LANDLOCK_KEY_INODE);
if (is_access_to_paths_allowed(dom, path, access_request, &layer_masks,
NULL, 0, NULL, NULL))
access_request = landlock_init_layer_masks(subject->domain,
access_request, &layer_masks,
LANDLOCK_KEY_INODE);
if (is_access_to_paths_allowed(subject->domain, path, access_request,
&layer_masks, &request, NULL, 0, NULL,
NULL, NULL))
return 0;
landlock_log_denial(subject, &request);
return -EACCES;
}
@ -1098,18 +1131,19 @@ static int current_check_refer_path(struct dentry *const old_dentry,
struct dentry *const new_dentry,
const bool removable, const bool exchange)
{
const struct landlock_ruleset *const dom = get_current_fs_domain();
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), any_fs, NULL);
bool allow_parent1, allow_parent2;
access_mask_t access_request_parent1, access_request_parent2;
struct path mnt_dir;
struct dentry *old_parent;
layer_mask_t layer_masks_parent1[LANDLOCK_NUM_ACCESS_FS] = {},
layer_masks_parent2[LANDLOCK_NUM_ACCESS_FS] = {};
struct landlock_request request1 = {}, request2 = {};
if (!dom)
if (!subject)
return 0;
if (WARN_ON_ONCE(dom->num_layers < 1))
return -EACCES;
if (unlikely(d_is_negative(old_dentry)))
return -ENOENT;
if (exchange) {
@ -1134,12 +1168,16 @@ static int current_check_refer_path(struct dentry *const old_dentry,
* for same-directory referer (i.e. no reparenting).
*/
access_request_parent1 = landlock_init_layer_masks(
dom, access_request_parent1 | access_request_parent2,
subject->domain,
access_request_parent1 | access_request_parent2,
&layer_masks_parent1, LANDLOCK_KEY_INODE);
if (is_access_to_paths_allowed(
dom, new_dir, access_request_parent1,
&layer_masks_parent1, NULL, 0, NULL, NULL))
if (is_access_to_paths_allowed(subject->domain, new_dir,
access_request_parent1,
&layer_masks_parent1, &request1,
NULL, 0, NULL, NULL, NULL))
return 0;
landlock_log_denial(subject, &request1);
return -EACCES;
}
@ -1160,10 +1198,12 @@ static int current_check_refer_path(struct dentry *const old_dentry,
old_dentry->d_parent;
/* new_dir->dentry is equal to new_dentry->d_parent */
allow_parent1 = collect_domain_accesses(dom, mnt_dir.dentry, old_parent,
allow_parent1 = collect_domain_accesses(subject->domain, mnt_dir.dentry,
old_parent,
&layer_masks_parent1);
allow_parent2 = collect_domain_accesses(
dom, mnt_dir.dentry, new_dir->dentry, &layer_masks_parent2);
allow_parent2 = collect_domain_accesses(subject->domain, mnt_dir.dentry,
new_dir->dentry,
&layer_masks_parent2);
if (allow_parent1 && allow_parent2)
return 0;
@ -1175,11 +1215,21 @@ static int current_check_refer_path(struct dentry *const old_dentry,
* destination parent access rights.
*/
if (is_access_to_paths_allowed(
dom, &mnt_dir, access_request_parent1, &layer_masks_parent1,
old_dentry, access_request_parent2, &layer_masks_parent2,
subject->domain, &mnt_dir, access_request_parent1,
&layer_masks_parent1, &request1, old_dentry,
access_request_parent2, &layer_masks_parent2, &request2,
exchange ? new_dentry : NULL))
return 0;
if (request1.access) {
request1.audit.u.path.dentry = old_parent;
landlock_log_denial(subject, &request1);
}
if (request2.access) {
request2.audit.u.path.dentry = new_dir->dentry;
landlock_log_denial(subject, &request2);
}
/*
* This prioritizes EACCES over EXDEV for all actions, including
* renames with RENAME_EXCHANGE.
@ -1322,6 +1372,34 @@ static void hook_sb_delete(struct super_block *const sb)
!atomic_long_read(&landlock_superblock(sb)->inode_refs));
}
static void
log_fs_change_topology_path(const struct landlock_cred_security *const subject,
size_t handle_layer, const struct path *const path)
{
landlock_log_denial(subject, &(struct landlock_request) {
.type = LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY,
.audit = {
.type = LSM_AUDIT_DATA_PATH,
.u.path = *path,
},
.layer_plus_one = handle_layer + 1,
});
}
static void log_fs_change_topology_dentry(
const struct landlock_cred_security *const subject, size_t handle_layer,
struct dentry *const dentry)
{
landlock_log_denial(subject, &(struct landlock_request) {
.type = LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY,
.audit = {
.type = LSM_AUDIT_DATA_DENTRY,
.u.dentry = dentry,
},
.layer_plus_one = handle_layer + 1,
});
}
/*
* Because a Landlock security policy is defined according to the filesystem
* topology (i.e. the mount namespace), changing it may grant access to files
@ -1344,16 +1422,30 @@ static int hook_sb_mount(const char *const dev_name,
const struct path *const path, const char *const type,
const unsigned long flags, void *const data)
{
if (!get_current_fs_domain())
size_t handle_layer;
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), any_fs,
&handle_layer);
if (!subject)
return 0;
log_fs_change_topology_path(subject, handle_layer, path);
return -EPERM;
}
static int hook_move_mount(const struct path *const from_path,
const struct path *const to_path)
{
if (!get_current_fs_domain())
size_t handle_layer;
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), any_fs,
&handle_layer);
if (!subject)
return 0;
log_fs_change_topology_path(subject, handle_layer, to_path);
return -EPERM;
}
@ -1363,15 +1455,29 @@ static int hook_move_mount(const struct path *const from_path,
*/
static int hook_sb_umount(struct vfsmount *const mnt, const int flags)
{
if (!get_current_fs_domain())
size_t handle_layer;
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), any_fs,
&handle_layer);
if (!subject)
return 0;
log_fs_change_topology_dentry(subject, handle_layer, mnt->mnt_root);
return -EPERM;
}
static int hook_sb_remount(struct super_block *const sb, void *const mnt_opts)
{
if (!get_current_fs_domain())
size_t handle_layer;
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), any_fs,
&handle_layer);
if (!subject)
return 0;
log_fs_change_topology_dentry(subject, handle_layer, sb->s_root);
return -EPERM;
}
@ -1386,8 +1492,15 @@ static int hook_sb_remount(struct super_block *const sb, void *const mnt_opts)
static int hook_sb_pivotroot(const struct path *const old_path,
const struct path *const new_path)
{
if (!get_current_fs_domain())
size_t handle_layer;
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), any_fs,
&handle_layer);
if (!subject)
return 0;
log_fs_change_topology_path(subject, handle_layer, new_path);
return -EPERM;
}
@ -1504,11 +1617,11 @@ static int hook_file_open(struct file *const file)
layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
access_mask_t open_access_request, full_access_request, allowed_access,
optional_access;
const struct landlock_ruleset *const dom =
landlock_get_applicable_domain(
landlock_cred(file->f_cred)->domain, any_fs);
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(file->f_cred, any_fs, NULL);
struct landlock_request request = {};
if (!dom)
if (!subject)
return 0;
/*
@ -1529,10 +1642,11 @@ static int hook_file_open(struct file *const file)
full_access_request = open_access_request | optional_access;
if (is_access_to_paths_allowed(
dom, &file->f_path,
landlock_init_layer_masks(dom, full_access_request,
&layer_masks, LANDLOCK_KEY_INODE),
&layer_masks, NULL, 0, NULL, NULL)) {
subject->domain, &file->f_path,
landlock_init_layer_masks(subject->domain,
full_access_request, &layer_masks,
LANDLOCK_KEY_INODE),
&layer_masks, &request, NULL, 0, NULL, NULL, NULL)) {
allowed_access = full_access_request;
} else {
unsigned long access_bit;
@ -1558,10 +1672,18 @@ static int hook_file_open(struct file *const file)
* file access rights in the opened struct file.
*/
landlock_file(file)->allowed_access = allowed_access;
#ifdef CONFIG_AUDIT
landlock_file(file)->deny_masks = landlock_get_deny_masks(
_LANDLOCK_ACCESS_FS_OPTIONAL, optional_access, &layer_masks,
ARRAY_SIZE(layer_masks));
#endif /* CONFIG_AUDIT */
if ((open_access_request & allowed_access) == open_access_request)
return 0;
/* Sets access to reflect the actual request. */
request.access = open_access_request;
landlock_log_denial(subject, &request);
return -EACCES;
}
@ -1579,76 +1701,131 @@ static int hook_file_truncate(struct file *const file)
*/
if (landlock_file(file)->allowed_access & LANDLOCK_ACCESS_FS_TRUNCATE)
return 0;
landlock_log_denial(landlock_cred(file->f_cred), &(struct landlock_request) {
.type = LANDLOCK_REQUEST_FS_ACCESS,
.audit = {
.type = LSM_AUDIT_DATA_FILE,
.u.file = file,
},
.all_existing_optional_access = _LANDLOCK_ACCESS_FS_OPTIONAL,
.access = LANDLOCK_ACCESS_FS_TRUNCATE,
#ifdef CONFIG_AUDIT
.deny_masks = landlock_file(file)->deny_masks,
#endif /* CONFIG_AUDIT */
});
return -EACCES;
}
static int hook_file_ioctl_common(const struct file *const file,
const unsigned int cmd, const bool is_compat)
{
access_mask_t allowed_access = landlock_file(file)->allowed_access;
/*
* It is the access rights at the time of opening the file which
* determine whether IOCTL can be used on the opened file later.
*
* The access right is attached to the opened file in hook_file_open().
*/
if (allowed_access & LANDLOCK_ACCESS_FS_IOCTL_DEV)
return 0;
if (!is_device(file))
return 0;
if (unlikely(is_compat) ? is_masked_device_ioctl_compat(cmd) :
is_masked_device_ioctl(cmd))
return 0;
landlock_log_denial(landlock_cred(file->f_cred), &(struct landlock_request) {
.type = LANDLOCK_REQUEST_FS_ACCESS,
.audit = {
.type = LSM_AUDIT_DATA_IOCTL_OP,
.u.op = &(struct lsm_ioctlop_audit) {
.path = file->f_path,
.cmd = cmd,
},
},
.all_existing_optional_access = _LANDLOCK_ACCESS_FS_OPTIONAL,
.access = LANDLOCK_ACCESS_FS_IOCTL_DEV,
#ifdef CONFIG_AUDIT
.deny_masks = landlock_file(file)->deny_masks,
#endif /* CONFIG_AUDIT */
});
return -EACCES;
}
static int hook_file_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
access_mask_t allowed_access = landlock_file(file)->allowed_access;
/*
* It is the access rights at the time of opening the file which
* determine whether IOCTL can be used on the opened file later.
*
* The access right is attached to the opened file in hook_file_open().
*/
if (allowed_access & LANDLOCK_ACCESS_FS_IOCTL_DEV)
return 0;
if (!is_device(file))
return 0;
if (is_masked_device_ioctl(cmd))
return 0;
return -EACCES;
return hook_file_ioctl_common(file, cmd, false);
}
static int hook_file_ioctl_compat(struct file *file, unsigned int cmd,
unsigned long arg)
{
access_mask_t allowed_access = landlock_file(file)->allowed_access;
/*
* It is the access rights at the time of opening the file which
* determine whether IOCTL can be used on the opened file later.
*
* The access right is attached to the opened file in hook_file_open().
*/
if (allowed_access & LANDLOCK_ACCESS_FS_IOCTL_DEV)
return 0;
if (!is_device(file))
return 0;
if (is_masked_device_ioctl_compat(cmd))
return 0;
return -EACCES;
return hook_file_ioctl_common(file, cmd, true);
}
static void hook_file_set_fowner(struct file *file)
/*
* Always allow sending signals between threads of the same process. This
* ensures consistency with hook_task_kill().
*/
static bool control_current_fowner(struct fown_struct *const fown)
{
struct landlock_ruleset *new_dom, *prev_dom;
struct task_struct *p;
/*
* Lock already held by __f_setown(), see commit 26f204380a3c ("fs: Fix
* file_set_fowner LSM hook inconsistencies").
*/
lockdep_assert_held(&file_f_owner(file)->lock);
new_dom = landlock_get_current_domain();
landlock_get_ruleset(new_dom);
prev_dom = landlock_file(file)->fown_domain;
landlock_file(file)->fown_domain = new_dom;
lockdep_assert_held(&fown->lock);
/* Called in an RCU read-side critical section. */
/*
* Some callers (e.g. fcntl_dirnotify) may not be in an RCU read-side
* critical section.
*/
guard(rcu)();
p = pid_task(fown->pid, fown->pid_type);
if (!p)
return true;
return !same_thread_group(p, current);
}
static void hook_file_set_fowner(struct file *file)
{
struct landlock_ruleset *prev_dom;
struct landlock_cred_security fown_subject = {};
size_t fown_layer = 0;
if (control_current_fowner(file_f_owner(file))) {
static const struct access_masks signal_scope = {
.scope = LANDLOCK_SCOPE_SIGNAL,
};
const struct landlock_cred_security *new_subject =
landlock_get_applicable_subject(
current_cred(), signal_scope, &fown_layer);
if (new_subject) {
landlock_get_ruleset(new_subject->domain);
fown_subject = *new_subject;
}
}
prev_dom = landlock_file(file)->fown_subject.domain;
landlock_file(file)->fown_subject = fown_subject;
#ifdef CONFIG_AUDIT
landlock_file(file)->fown_layer = fown_layer;
#endif /* CONFIG_AUDIT*/
/* May be called in an RCU read-side critical section. */
landlock_put_ruleset_deferred(prev_dom);
}
static void hook_file_free_security(struct file *file)
{
landlock_put_ruleset_deferred(landlock_file(file)->fown_domain);
landlock_put_ruleset_deferred(landlock_file(file)->fown_subject.domain);
}
static struct security_hook_list landlock_hooks[] __ro_after_init = {

View File

@ -1,19 +1,22 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Landlock LSM - Filesystem management and hooks
* Landlock - Filesystem management and hooks
*
* Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
* Copyright © 2024-2025 Microsoft Corporation
*/
#ifndef _SECURITY_LANDLOCK_FS_H
#define _SECURITY_LANDLOCK_FS_H
#include <linux/build_bug.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/rcupdate.h>
#include "access.h"
#include "cred.h"
#include "ruleset.h"
#include "setup.h"
@ -53,15 +56,40 @@ struct landlock_file_security {
* needed to authorize later operations on the open file.
*/
access_mask_t allowed_access;
#ifdef CONFIG_AUDIT
/**
* @fown_domain: Domain of the task that set the PID that may receive a
* signal e.g., SIGURG when writing MSG_OOB to the related socket.
* This pointer is protected by the related file->f_owner->lock, as for
* fown_struct's members: pid, uid, and euid.
* @deny_masks: Domain layer levels that deny an optional access (see
* _LANDLOCK_ACCESS_FS_OPTIONAL).
*/
struct landlock_ruleset *fown_domain;
deny_masks_t deny_masks;
/**
* @fown_layer: Layer level of @fown_subject->domain with
* LANDLOCK_SCOPE_SIGNAL.
*/
u8 fown_layer;
#endif /* CONFIG_AUDIT */
/**
* @fown_subject: Landlock credential of the task that set the PID that
* may receive a signal e.g., SIGURG when writing MSG_OOB to the
* related socket. This pointer is protected by the related
* file->f_owner->lock, as for fown_struct's members: pid, uid, and
* euid.
*/
struct landlock_cred_security fown_subject;
};
#ifdef CONFIG_AUDIT
/* Makes sure all layers can be identified. */
/* clang-format off */
static_assert((typeof_member(struct landlock_file_security, fown_layer))~0 >=
LANDLOCK_MAX_NUM_LAYERS);
/* clang-format off */
#endif /* CONFIG_AUDIT */
/**
* struct landlock_superblock_security - Superblock security blob
*

251
security/landlock/id.c Normal file
View File

@ -0,0 +1,251 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Landlock - Unique identification number generator
*
* Copyright © 2024-2025 Microsoft Corporation
*/
#include <kunit/test.h>
#include <linux/atomic.h>
#include <linux/random.h>
#include <linux/spinlock.h>
#include "common.h"
#include "id.h"
#define COUNTER_PRE_INIT 0
static atomic64_t next_id = ATOMIC64_INIT(COUNTER_PRE_INIT);
static void __init init_id(atomic64_t *const counter, const u32 random_32bits)
{
u64 init;
/*
* Ensures sure 64-bit values are always used by user space (or may
* fail with -EOVERFLOW), and makes this testable.
*/
init = 1ULL << 32;
/*
* Makes a large (2^32) boot-time value to limit ID collision in logs
* from different boots, and to limit info leak about the number of
* initially (relative to the reader) created elements (e.g. domains).
*/
init += random_32bits;
/* Sets first or ignores. This will be the first ID. */
atomic64_cmpxchg(counter, COUNTER_PRE_INIT, init);
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static void __init test_init_min(struct kunit *const test)
{
atomic64_t counter = ATOMIC64_INIT(COUNTER_PRE_INIT);
init_id(&counter, 0);
KUNIT_EXPECT_EQ(test, atomic64_read(&counter), 1ULL + U32_MAX);
}
static void __init test_init_max(struct kunit *const test)
{
atomic64_t counter = ATOMIC64_INIT(COUNTER_PRE_INIT);
init_id(&counter, ~0);
KUNIT_EXPECT_EQ(test, atomic64_read(&counter), 1 + (2ULL * U32_MAX));
}
static void __init test_init_once(struct kunit *const test)
{
const u64 first_init = 1ULL + U32_MAX;
atomic64_t counter = ATOMIC64_INIT(COUNTER_PRE_INIT);
init_id(&counter, 0);
KUNIT_EXPECT_EQ(test, atomic64_read(&counter), first_init);
init_id(&counter, ~0);
KUNIT_EXPECT_EQ_MSG(
test, atomic64_read(&counter), first_init,
"Should still have the same value after the subsequent init_id()");
}
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
void __init landlock_init_id(void)
{
return init_id(&next_id, get_random_u32());
}
/*
* It's not worth it to try to hide the monotonic counter because it can still
* be inferred (with N counter ranges), and if we are allowed to read the inode
* number we should also be allowed to read the time creation anyway, and it
* can be handy to store and sort domain IDs for user space.
*
* Returns the value of next_id and increment it to let some space for the next
* one.
*/
static u64 get_id_range(size_t number_of_ids, atomic64_t *const counter,
u8 random_4bits)
{
u64 id, step;
/*
* We should return at least 1 ID, and we may need a set of consecutive
* ones (e.g. to generate a set of inodes).
*/
if (WARN_ON_ONCE(number_of_ids <= 0))
number_of_ids = 1;
/*
* Blurs the next ID guess with 1/16 ratio. We get 2^(64 - 4) -
* (2 * 2^32), so a bit less than 2^60 available IDs, which should be
* much more than enough considering the number of CPU cycles required
* to get a new ID (e.g. a full landlock_restrict_self() call), and the
* cost of draining all available IDs during the system's uptime.
*/
random_4bits = random_4bits % (1 << 4);
step = number_of_ids + random_4bits;
/* It is safe to cast a signed atomic to an unsigned value. */
id = atomic64_fetch_add(step, counter);
/* Warns if landlock_init_id() was not called. */
WARN_ON_ONCE(id == COUNTER_PRE_INIT);
return id;
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static void test_range1_rand0(struct kunit *const test)
{
atomic64_t counter;
u64 init;
init = get_random_u32();
atomic64_set(&counter, init);
KUNIT_EXPECT_EQ(test, get_id_range(1, &counter, 0), init);
KUNIT_EXPECT_EQ(
test, get_id_range(get_random_u8(), &counter, get_random_u8()),
init + 1);
}
static void test_range1_rand1(struct kunit *const test)
{
atomic64_t counter;
u64 init;
init = get_random_u32();
atomic64_set(&counter, init);
KUNIT_EXPECT_EQ(test, get_id_range(1, &counter, 1), init);
KUNIT_EXPECT_EQ(
test, get_id_range(get_random_u8(), &counter, get_random_u8()),
init + 2);
}
static void test_range1_rand16(struct kunit *const test)
{
atomic64_t counter;
u64 init;
init = get_random_u32();
atomic64_set(&counter, init);
KUNIT_EXPECT_EQ(test, get_id_range(1, &counter, 16), init);
KUNIT_EXPECT_EQ(
test, get_id_range(get_random_u8(), &counter, get_random_u8()),
init + 1);
}
static void test_range2_rand0(struct kunit *const test)
{
atomic64_t counter;
u64 init;
init = get_random_u32();
atomic64_set(&counter, init);
KUNIT_EXPECT_EQ(test, get_id_range(2, &counter, 0), init);
KUNIT_EXPECT_EQ(
test, get_id_range(get_random_u8(), &counter, get_random_u8()),
init + 2);
}
static void test_range2_rand1(struct kunit *const test)
{
atomic64_t counter;
u64 init;
init = get_random_u32();
atomic64_set(&counter, init);
KUNIT_EXPECT_EQ(test, get_id_range(2, &counter, 1), init);
KUNIT_EXPECT_EQ(
test, get_id_range(get_random_u8(), &counter, get_random_u8()),
init + 3);
}
static void test_range2_rand2(struct kunit *const test)
{
atomic64_t counter;
u64 init;
init = get_random_u32();
atomic64_set(&counter, init);
KUNIT_EXPECT_EQ(test, get_id_range(2, &counter, 2), init);
KUNIT_EXPECT_EQ(
test, get_id_range(get_random_u8(), &counter, get_random_u8()),
init + 4);
}
static void test_range2_rand16(struct kunit *const test)
{
atomic64_t counter;
u64 init;
init = get_random_u32();
atomic64_set(&counter, init);
KUNIT_EXPECT_EQ(test, get_id_range(2, &counter, 16), init);
KUNIT_EXPECT_EQ(
test, get_id_range(get_random_u8(), &counter, get_random_u8()),
init + 2);
}
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
/**
* landlock_get_id_range - Get a range of unique IDs
*
* @number_of_ids: Number of IDs to hold. Must be greater than one.
*
* Returns: The first ID in the range.
*/
u64 landlock_get_id_range(size_t number_of_ids)
{
return get_id_range(number_of_ids, &next_id, get_random_u8());
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static struct kunit_case __refdata test_cases[] = {
/* clang-format off */
KUNIT_CASE(test_init_min),
KUNIT_CASE(test_init_max),
KUNIT_CASE(test_init_once),
KUNIT_CASE(test_range1_rand0),
KUNIT_CASE(test_range1_rand1),
KUNIT_CASE(test_range1_rand16),
KUNIT_CASE(test_range2_rand0),
KUNIT_CASE(test_range2_rand1),
KUNIT_CASE(test_range2_rand2),
KUNIT_CASE(test_range2_rand16),
{}
/* clang-format on */
};
static struct kunit_suite test_suite = {
.name = "landlock_id",
.test_cases = test_cases,
};
kunit_test_init_section_suite(test_suite);
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */

25
security/landlock/id.h Normal file
View File

@ -0,0 +1,25 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Landlock - Unique identification number generator
*
* Copyright © 2024-2025 Microsoft Corporation
*/
#ifndef _SECURITY_LANDLOCK_ID_H
#define _SECURITY_LANDLOCK_ID_H
#ifdef CONFIG_AUDIT
void __init landlock_init_id(void);
u64 landlock_get_id_range(size_t number_of_ids);
#else /* CONFIG_AUDIT */
static inline void __init landlock_init_id(void)
{
}
#endif /* CONFIG_AUDIT */
#endif /* _SECURITY_LANDLOCK_ID_H */

View File

@ -1,9 +1,10 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Landlock LSM - Limits for different components
* Landlock - Limits for different components
*
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
* Copyright © 2021-2025 Microsoft Corporation
*/
#ifndef _SECURITY_LANDLOCK_LIMITS_H
@ -29,6 +30,10 @@
#define LANDLOCK_LAST_SCOPE LANDLOCK_SCOPE_SIGNAL
#define LANDLOCK_MASK_SCOPE ((LANDLOCK_LAST_SCOPE << 1) - 1)
#define LANDLOCK_NUM_SCOPE __const_hweight64(LANDLOCK_MASK_SCOPE)
#define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF
#define LANDLOCK_MASK_RESTRICT_SELF ((LANDLOCK_LAST_RESTRICT_SELF << 1) - 1)
/* clang-format on */
#endif /* _SECURITY_LANDLOCK_LIMITS_H */

View File

@ -1,16 +1,18 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Landlock LSM - Network management and hooks
* Landlock - Network management and hooks
*
* Copyright © 2022-2023 Huawei Tech. Co., Ltd.
* Copyright © 2022-2023 Microsoft Corporation
* Copyright © 2022-2025 Microsoft Corporation
*/
#include <linux/in.h>
#include <linux/lsm_audit.h>
#include <linux/net.h>
#include <linux/socket.h>
#include <net/ipv6.h>
#include "audit.h"
#include "common.h"
#include "cred.h"
#include "limits.h"
@ -39,10 +41,6 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
return err;
}
static const struct access_masks any_net = {
.net = ~0,
};
static int current_check_access_socket(struct socket *const sock,
struct sockaddr *const address,
const int addrlen,
@ -54,14 +52,15 @@ static int current_check_access_socket(struct socket *const sock,
struct landlock_id id = {
.type = LANDLOCK_KEY_NET_PORT,
};
const struct landlock_ruleset *const dom =
landlock_get_applicable_domain(landlock_get_current_domain(),
any_net);
const struct access_masks masks = {
.net = access_request,
};
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), masks, NULL);
struct lsm_network_audit audit_net = {};
if (!dom)
if (!subject)
return 0;
if (WARN_ON_ONCE(dom->num_layers < 1))
return -EACCES;
if (!sk_is_tcp(sock->sk))
return 0;
@ -72,18 +71,48 @@ static int current_check_access_socket(struct socket *const sock,
switch (address->sa_family) {
case AF_UNSPEC:
case AF_INET:
case AF_INET: {
const struct sockaddr_in *addr4;
if (addrlen < sizeof(struct sockaddr_in))
return -EINVAL;
port = ((struct sockaddr_in *)address)->sin_port;
addr4 = (struct sockaddr_in *)address;
port = addr4->sin_port;
if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP) {
audit_net.dport = port;
audit_net.v4info.daddr = addr4->sin_addr.s_addr;
} else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP) {
audit_net.sport = port;
audit_net.v4info.saddr = addr4->sin_addr.s_addr;
} else {
WARN_ON_ONCE(1);
}
break;
}
#if IS_ENABLED(CONFIG_IPV6)
case AF_INET6:
case AF_INET6: {
const struct sockaddr_in6 *addr6;
if (addrlen < SIN6_LEN_RFC2133)
return -EINVAL;
port = ((struct sockaddr_in6 *)address)->sin6_port;
addr6 = (struct sockaddr_in6 *)address;
port = addr6->sin6_port;
if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP) {
audit_net.dport = port;
audit_net.v6info.daddr = addr6->sin6_addr;
} else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP) {
audit_net.sport = port;
audit_net.v6info.saddr = addr6->sin6_addr;
} else {
WARN_ON_ONCE(1);
}
break;
}
#endif /* IS_ENABLED(CONFIG_IPV6) */
default:
@ -145,13 +174,24 @@ static int current_check_access_socket(struct socket *const sock,
id.key.data = (__force uintptr_t)port;
BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data));
rule = landlock_find_rule(dom, id);
access_request = landlock_init_layer_masks(
dom, access_request, &layer_masks, LANDLOCK_KEY_NET_PORT);
rule = landlock_find_rule(subject->domain, id);
access_request = landlock_init_layer_masks(subject->domain,
access_request, &layer_masks,
LANDLOCK_KEY_NET_PORT);
if (landlock_unmask_layers(rule, access_request, &layer_masks,
ARRAY_SIZE(layer_masks)))
return 0;
audit_net.family = address->sa_family;
landlock_log_denial(subject,
&(struct landlock_request){
.type = LANDLOCK_REQUEST_NET_ACCESS,
.audit.type = LSM_AUDIT_DATA_NET,
.audit.u.net = &audit_net,
.access = access_request,
.layer_masks = &layer_masks,
.layer_masks_size = ARRAY_SIZE(layer_masks),
});
return -EACCES;
}

View File

@ -23,6 +23,8 @@
#include <linux/workqueue.h>
#include "access.h"
#include "audit.h"
#include "domain.h"
#include "limits.h"
#include "object.h"
#include "ruleset.h"
@ -307,22 +309,6 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
return insert_rule(ruleset, id, &layers, ARRAY_SIZE(layers));
}
static void get_hierarchy(struct landlock_hierarchy *const hierarchy)
{
if (hierarchy)
refcount_inc(&hierarchy->usage);
}
static void put_hierarchy(struct landlock_hierarchy *hierarchy)
{
while (hierarchy && refcount_dec_and_test(&hierarchy->usage)) {
const struct landlock_hierarchy *const freeme = hierarchy;
hierarchy = hierarchy->parent;
kfree(freeme);
}
}
static int merge_tree(struct landlock_ruleset *const dst,
struct landlock_ruleset *const src,
const enum landlock_key_type key_type)
@ -477,7 +463,7 @@ static int inherit_ruleset(struct landlock_ruleset *const parent,
err = -EINVAL;
goto out_unlock;
}
get_hierarchy(parent->hierarchy);
landlock_get_hierarchy(parent->hierarchy);
child->hierarchy->parent = parent->hierarchy;
out_unlock:
@ -501,7 +487,7 @@ static void free_ruleset(struct landlock_ruleset *const ruleset)
free_rule(freeme, LANDLOCK_KEY_NET_PORT);
#endif /* IS_ENABLED(CONFIG_INET) */
put_hierarchy(ruleset->hierarchy);
landlock_put_hierarchy(ruleset->hierarchy);
kfree(ruleset);
}
@ -520,6 +506,7 @@ static void free_ruleset_work(struct work_struct *const work)
free_ruleset(ruleset);
}
/* Only called by hook_cred_free(). */
void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset)
{
if (ruleset && refcount_dec_and_test(&ruleset->usage)) {
@ -534,6 +521,9 @@ void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset)
* @parent: Parent domain.
* @ruleset: New ruleset to be merged.
*
* The current task is requesting to be restricted. The subjective credentials
* must not be in an overridden state. cf. landlock_init_hierarchy_log().
*
* Returns the intersection of @parent and @ruleset, or returns @parent if
* @ruleset is empty, or returns a duplicate of @ruleset if @parent is empty.
*/
@ -579,6 +569,10 @@ landlock_merge_ruleset(struct landlock_ruleset *const parent,
if (err)
return ERR_PTR(err);
err = landlock_init_hierarchy_log(new_dom->hierarchy);
if (err)
return ERR_PTR(err);
return no_free_ptr(new_dom);
}

View File

@ -20,6 +20,8 @@
#include "limits.h"
#include "object.h"
struct landlock_hierarchy;
/**
* struct landlock_layer - Access rights for a given layer
*/
@ -108,22 +110,6 @@ struct landlock_rule {
struct landlock_layer layers[] __counted_by(num_layers);
};
/**
* struct landlock_hierarchy - Node in a ruleset hierarchy
*/
struct landlock_hierarchy {
/**
* @parent: Pointer to the parent node, or NULL if it is a root
* Landlock domain.
*/
struct landlock_hierarchy *parent;
/**
* @usage: Number of potential children domains plus their parent
* domain.
*/
refcount_t usage;
};
/**
* struct landlock_ruleset - Landlock ruleset
*
@ -257,36 +243,6 @@ landlock_union_access_masks(const struct landlock_ruleset *const domain)
return matches.masks;
}
/**
* landlock_get_applicable_domain - Return @domain if it applies to (handles)
* at least one of the access rights specified
* in @masks
*
* @domain: Landlock ruleset (used as a domain)
* @masks: access masks
*
* Returns: @domain if any access rights specified in @masks is handled, or
* NULL otherwise.
*/
static inline const struct landlock_ruleset *
landlock_get_applicable_domain(const struct landlock_ruleset *const domain,
const struct access_masks masks)
{
const union access_masks_all masks_all = {
.masks = masks,
};
union access_masks_all merge = {};
if (!domain)
return NULL;
merge.masks = landlock_union_access_masks(domain);
if (merge.all & masks_all.all)
return domain;
return NULL;
}
static inline void
landlock_add_fs_access_mask(struct landlock_ruleset *const ruleset,
const access_mask_t fs_access_mask,

View File

@ -6,19 +6,27 @@
* Copyright © 2018-2020 ANSSI
*/
#include <linux/bits.h>
#include <linux/init.h>
#include <linux/lsm_hooks.h>
#include <uapi/linux/lsm.h>
#include "common.h"
#include "cred.h"
#include "errata.h"
#include "fs.h"
#include "id.h"
#include "net.h"
#include "setup.h"
#include "task.h"
bool landlock_initialized __ro_after_init = false;
const struct lsm_id landlock_lsmid = {
.name = LANDLOCK_NAME,
.id = LSM_ID_LANDLOCK,
};
struct lsm_blob_sizes landlock_blob_sizes __ro_after_init = {
.lbs_cred = sizeof(struct landlock_cred_security),
.lbs_file = sizeof(struct landlock_file_security),
@ -26,17 +34,41 @@ struct lsm_blob_sizes landlock_blob_sizes __ro_after_init = {
.lbs_superblock = sizeof(struct landlock_superblock_security),
};
const struct lsm_id landlock_lsmid = {
.name = LANDLOCK_NAME,
.id = LSM_ID_LANDLOCK,
};
int landlock_errata __ro_after_init;
static void __init compute_errata(void)
{
size_t i;
#ifndef __has_include
/*
* This is a safeguard to make sure the compiler implements
* __has_include (see errata.h).
*/
WARN_ON_ONCE(1);
return;
#endif
for (i = 0; landlock_errata_init[i].number; i++) {
const int prev_errata = landlock_errata;
if (WARN_ON_ONCE(landlock_errata_init[i].abi >
landlock_abi_version))
continue;
landlock_errata |= BIT(landlock_errata_init[i].number - 1);
WARN_ON_ONCE(prev_errata == landlock_errata);
}
}
static int __init landlock_init(void)
{
compute_errata();
landlock_add_cred_hooks();
landlock_add_task_hooks();
landlock_add_fs_hooks();
landlock_add_net_hooks();
landlock_init_id();
landlock_initialized = true;
pr_info("Up and running.\n");
return 0;

View File

@ -11,7 +11,10 @@
#include <linux/lsm_hooks.h>
extern const int landlock_abi_version;
extern bool landlock_initialized;
extern int landlock_errata;
extern struct lsm_blob_sizes landlock_blob_sizes;
extern const struct lsm_id landlock_lsmid;

View File

@ -1,9 +1,10 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Landlock LSM - System call implementations and user space interfaces
* Landlock - System call implementations and user space interfaces
*
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
* Copyright © 2021-2025 Microsoft Corporation
*/
#include <asm/current.h>
@ -28,6 +29,7 @@
#include <uapi/linux/landlock.h>
#include "cred.h"
#include "domain.h"
#include "fs.h"
#include "limits.h"
#include "net.h"
@ -151,7 +153,14 @@ static const struct file_operations ruleset_fops = {
.write = fop_dummy_write,
};
#define LANDLOCK_ABI_VERSION 6
/*
* The Landlock ABI version should be incremented for each new Landlock-related
* user space visible change (e.g. Landlock syscalls). This version should
* only be incremented once per Linux release, and the date in
* Documentation/userspace-api/landlock.rst should be updated to reflect the
* UAPI change.
*/
const int landlock_abi_version = 7;
/**
* sys_landlock_create_ruleset - Create a new ruleset
@ -160,7 +169,9 @@ static const struct file_operations ruleset_fops = {
* the new ruleset.
* @size: Size of the pointed &struct landlock_ruleset_attr (needed for
* backward and forward compatibility).
* @flags: Supported value: %LANDLOCK_CREATE_RULESET_VERSION.
* @flags: Supported value:
* - %LANDLOCK_CREATE_RULESET_VERSION
* - %LANDLOCK_CREATE_RULESET_ERRATA
*
* This system call enables to create a new Landlock ruleset, and returns the
* related file descriptor on success.
@ -169,6 +180,10 @@ static const struct file_operations ruleset_fops = {
* 0, then the returned value is the highest supported Landlock ABI version
* (starting at 1).
*
* If @flags is %LANDLOCK_CREATE_RULESET_ERRATA and @attr is NULL and @size is
* 0, then the returned value is a bitmask of fixed issues for the current
* Landlock ABI version.
*
* Possible returned errors are:
*
* - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
@ -192,9 +207,15 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
return -EOPNOTSUPP;
if (flags) {
if ((flags == LANDLOCK_CREATE_RULESET_VERSION) && !attr &&
!size)
return LANDLOCK_ABI_VERSION;
if (attr || size)
return -EINVAL;
if (flags == LANDLOCK_CREATE_RULESET_VERSION)
return landlock_abi_version;
if (flags == LANDLOCK_CREATE_RULESET_ERRATA)
return landlock_errata;
return -EINVAL;
}
@ -429,17 +450,24 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
* sys_landlock_restrict_self - Enforce a ruleset on the calling thread
*
* @ruleset_fd: File descriptor tied to the ruleset to merge with the target.
* @flags: Must be 0.
* @flags: Supported values:
*
* - %LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF
* - %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON
* - %LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF
*
* This system call enables to enforce a Landlock ruleset on the current
* thread. Enforcing a ruleset requires that the task has %CAP_SYS_ADMIN in its
* namespace or is running with no_new_privs. This avoids scenarios where
* unprivileged tasks can affect the behavior of privileged children.
*
* It is allowed to only pass the %LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF
* flag with a @ruleset_fd value of -1.
*
* Possible returned errors are:
*
* - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
* - %EINVAL: @flags is not 0.
* - %EINVAL: @flags contains an unknown bit.
* - %EBADF: @ruleset_fd is not a file descriptor for the current thread;
* - %EBADFD: @ruleset_fd is not a ruleset file descriptor;
* - %EPERM: @ruleset_fd has no read access to the underlying ruleset, or the
@ -455,6 +483,8 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
*ruleset __free(landlock_put_ruleset) = NULL;
struct cred *new_cred;
struct landlock_cred_security *new_llcred;
bool __maybe_unused log_same_exec, log_new_exec, log_subdomains,
prev_log_subdomains;
if (!is_initialized())
return -EOPNOTSUPP;
@ -467,14 +497,28 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
!ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))
return -EPERM;
/* No flag for now. */
if (flags)
if ((flags | LANDLOCK_MASK_RESTRICT_SELF) !=
LANDLOCK_MASK_RESTRICT_SELF)
return -EINVAL;
/* Gets and checks the ruleset. */
ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ);
if (IS_ERR(ruleset))
return PTR_ERR(ruleset);
/* Translates "off" flag to boolean. */
log_same_exec = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF);
/* Translates "on" flag to boolean. */
log_new_exec = !!(flags & LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON);
/* Translates "off" flag to boolean. */
log_subdomains = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF);
/*
* It is allowed to set LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with
* -1 as ruleset_fd, but no other flag must be set.
*/
if (!(ruleset_fd == -1 &&
flags == LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
/* Gets and checks the ruleset. */
ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ);
if (IS_ERR(ruleset))
return PTR_ERR(ruleset);
}
/* Prepares new credentials. */
new_cred = prepare_creds();
@ -483,6 +527,21 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
new_llcred = landlock_cred(new_cred);
#ifdef CONFIG_AUDIT
prev_log_subdomains = !new_llcred->log_subdomains_off;
new_llcred->log_subdomains_off = !prev_log_subdomains ||
!log_subdomains;
#endif /* CONFIG_AUDIT */
/*
* The only case when a ruleset may not be set is if
* LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set and ruleset_fd is -1.
* We could optimize this case by not calling commit_creds() if this flag
* was already set, but it is not worth the complexity.
*/
if (!ruleset)
return commit_creds(new_cred);
/*
* There is no possible race condition while copying and manipulating
* the current credentials because they are dedicated per thread.
@ -493,8 +552,20 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
return PTR_ERR(new_dom);
}
#ifdef CONFIG_AUDIT
new_dom->hierarchy->log_same_exec = log_same_exec;
new_dom->hierarchy->log_new_exec = log_new_exec;
if ((!log_same_exec && !log_new_exec) || !prev_log_subdomains)
new_dom->hierarchy->log_status = LANDLOCK_LOG_DISABLED;
#endif /* CONFIG_AUDIT */
/* Replaces the old (prepared) domain. */
landlock_put_ruleset(new_llcred->domain);
new_llcred->domain = new_dom;
#ifdef CONFIG_AUDIT
new_llcred->domain_exec |= 1 << (new_dom->num_layers - 1);
#endif /* CONFIG_AUDIT */
return commit_creds(new_cred);
}

View File

@ -1,23 +1,29 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Landlock LSM - Ptrace hooks
* Landlock - Ptrace and scope hooks
*
* Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2019-2020 ANSSI
* Copyright © 2024-2025 Microsoft Corporation
*/
#include <asm/current.h>
#include <linux/cleanup.h>
#include <linux/cred.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/lsm_audit.h>
#include <linux/lsm_hooks.h>
#include <linux/rcupdate.h>
#include <linux/sched.h>
#include <linux/sched/signal.h>
#include <net/af_unix.h>
#include <net/sock.h>
#include "audit.h"
#include "common.h"
#include "cred.h"
#include "domain.h"
#include "fs.h"
#include "ruleset.h"
#include "setup.h"
@ -37,41 +43,29 @@ static bool domain_scope_le(const struct landlock_ruleset *const parent,
{
const struct landlock_hierarchy *walker;
/* Quick return for non-landlocked tasks. */
if (!parent)
return true;
if (!child)
return false;
for (walker = child->hierarchy; walker; walker = walker->parent) {
if (walker == parent->hierarchy)
/* @parent is in the scoped hierarchy of @child. */
return true;
}
/* There is no relationship between @parent and @child. */
return false;
}
static bool task_is_scoped(const struct task_struct *const parent,
const struct task_struct *const child)
static int domain_ptrace(const struct landlock_ruleset *const parent,
const struct landlock_ruleset *const child)
{
bool is_scoped;
const struct landlock_ruleset *dom_parent, *dom_child;
rcu_read_lock();
dom_parent = landlock_get_task_domain(parent);
dom_child = landlock_get_task_domain(child);
is_scoped = domain_scope_le(dom_parent, dom_child);
rcu_read_unlock();
return is_scoped;
}
static int task_ptrace(const struct task_struct *const parent,
const struct task_struct *const child)
{
/* Quick return for non-landlocked tasks. */
if (!landlocked(parent))
return 0;
if (task_is_scoped(parent, child))
if (domain_scope_le(parent, child))
return 0;
return -EPERM;
}
@ -91,7 +85,39 @@ static int task_ptrace(const struct task_struct *const parent,
static int hook_ptrace_access_check(struct task_struct *const child,
const unsigned int mode)
{
return task_ptrace(current, child);
const struct landlock_cred_security *parent_subject;
const struct landlock_ruleset *child_dom;
int err;
/* Quick return for non-landlocked tasks. */
parent_subject = landlock_cred(current_cred());
if (!parent_subject)
return 0;
scoped_guard(rcu)
{
child_dom = landlock_get_task_domain(child);
err = domain_ptrace(parent_subject->domain, child_dom);
}
if (!err)
return 0;
/*
* For the ptrace_access_check case, we log the current/parent domain
* and the child task.
*/
if (!(mode & PTRACE_MODE_NOAUDIT))
landlock_log_denial(parent_subject, &(struct landlock_request) {
.type = LANDLOCK_REQUEST_PTRACE,
.audit = {
.type = LSM_AUDIT_DATA_TASK,
.u.tsk = child,
},
.layer_plus_one = parent_subject->domain->num_layers,
});
return err;
}
/**
@ -108,7 +134,35 @@ static int hook_ptrace_access_check(struct task_struct *const child,
*/
static int hook_ptrace_traceme(struct task_struct *const parent)
{
return task_ptrace(parent, current);
const struct landlock_cred_security *parent_subject;
const struct landlock_ruleset *child_dom;
int err;
child_dom = landlock_get_current_domain();
guard(rcu)();
parent_subject = landlock_cred(__task_cred(parent));
err = domain_ptrace(parent_subject->domain, child_dom);
if (!err)
return 0;
/*
* For the ptrace_traceme case, we log the domain which is the cause of
* the denial, which means the parent domain instead of the current
* domain. This may look unusual because the ptrace_traceme action is a
* request to be traced, but the semantic is consistent with
* hook_ptrace_access_check().
*/
landlock_log_denial(parent_subject, &(struct landlock_request) {
.type = LANDLOCK_REQUEST_PTRACE,
.audit = {
.type = LSM_AUDIT_DATA_TASK,
.u.tsk = current,
},
.layer_plus_one = parent_subject->domain->num_layers,
});
return err;
}
/**
@ -127,7 +181,7 @@ static bool domain_is_scoped(const struct landlock_ruleset *const client,
access_mask_t scope)
{
int client_layer, server_layer;
struct landlock_hierarchy *client_walker, *server_walker;
const struct landlock_hierarchy *client_walker, *server_walker;
/* Quick return if client has no domain */
if (WARN_ON_ONCE(!client))
@ -212,28 +266,43 @@ static int hook_unix_stream_connect(struct sock *const sock,
struct sock *const other,
struct sock *const newsk)
{
const struct landlock_ruleset *const dom =
landlock_get_applicable_domain(landlock_get_current_domain(),
unix_scope);
size_t handle_layer;
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), unix_scope,
&handle_layer);
/* Quick return for non-landlocked tasks. */
if (!dom)
if (!subject)
return 0;
if (is_abstract_socket(other) && sock_is_scoped(other, dom))
return -EPERM;
if (!is_abstract_socket(other))
return 0;
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;
}
static int hook_unix_may_send(struct socket *const sock,
struct socket *const other)
{
const struct landlock_ruleset *const dom =
landlock_get_applicable_domain(landlock_get_current_domain(),
unix_scope);
size_t handle_layer;
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), unix_scope,
&handle_layer);
if (!dom)
if (!subject)
return 0;
/*
@ -243,10 +312,23 @@ 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) && sock_is_scoped(other->sk, dom))
return -EPERM;
if (!is_abstract_socket(other->sk))
return 0;
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;
}
static const struct access_masks signal_scope = {
@ -255,56 +337,97 @@ static const struct access_masks signal_scope = {
static int hook_task_kill(struct task_struct *const p,
struct kernel_siginfo *const info, const int sig,
const struct cred *const cred)
const struct cred *cred)
{
bool is_scoped;
const struct landlock_ruleset *dom;
size_t handle_layer;
const struct landlock_cred_security *subject;
if (cred) {
/* Dealing with USB IO. */
dom = landlock_cred(cred)->domain;
} else {
dom = landlock_get_current_domain();
if (!cred) {
/*
* Always allow sending signals between threads of the same process.
* This is required for process credential changes by the Native POSIX
* Threads Library and implemented by the set*id(2) wrappers and
* libcap(3) with tgkill(2). See nptl(7) and libpsx(3).
*
* This exception is similar to the __ptrace_may_access() one.
*/
if (same_thread_group(p, current))
return 0;
/* Not dealing with USB IO. */
cred = current_cred();
}
dom = landlock_get_applicable_domain(dom, signal_scope);
subject = landlock_get_applicable_subject(cred, signal_scope,
&handle_layer);
/* Quick return for non-landlocked tasks. */
if (!dom)
if (!subject)
return 0;
rcu_read_lock();
is_scoped = domain_is_scoped(dom, landlock_get_task_domain(p),
LANDLOCK_SCOPE_SIGNAL);
rcu_read_unlock();
if (is_scoped)
return -EPERM;
scoped_guard(rcu)
{
is_scoped = domain_is_scoped(subject->domain,
landlock_get_task_domain(p),
signal_scope.scope);
}
return 0;
if (!is_scoped)
return 0;
landlock_log_denial(subject, &(struct landlock_request) {
.type = LANDLOCK_REQUEST_SCOPE_SIGNAL,
.audit = {
.type = LSM_AUDIT_DATA_TASK,
.u.tsk = p,
},
.layer_plus_one = handle_layer + 1,
});
return -EPERM;
}
static int hook_file_send_sigiotask(struct task_struct *tsk,
struct fown_struct *fown, int signum)
{
const struct landlock_ruleset *dom;
const struct landlock_cred_security *subject;
bool is_scoped = false;
/* Lock already held by send_sigio() and send_sigurg(). */
lockdep_assert_held(&fown->lock);
dom = landlock_get_applicable_domain(
landlock_file(fown->file)->fown_domain, signal_scope);
subject = &landlock_file(fown->file)->fown_subject;
/* Quick return for unowned socket. */
if (!dom)
/*
* Quick return for unowned socket.
*
* subject->domain has already been filtered when saved by
* hook_file_set_fowner(), so there is no need to call
* landlock_get_applicable_subject() here.
*/
if (!subject->domain)
return 0;
rcu_read_lock();
is_scoped = domain_is_scoped(dom, landlock_get_task_domain(tsk),
LANDLOCK_SCOPE_SIGNAL);
rcu_read_unlock();
if (is_scoped)
return -EPERM;
scoped_guard(rcu)
{
is_scoped = domain_is_scoped(subject->domain,
landlock_get_task_domain(tsk),
signal_scope.scope);
}
return 0;
if (!is_scoped)
return 0;
landlock_log_denial(subject, &(struct landlock_request) {
.type = LANDLOCK_REQUEST_SCOPE_SIGNAL,
.audit = {
.type = LSM_AUDIT_DATA_TASK,
.u.tsk = tsk,
},
#ifdef CONFIG_AUDIT
.layer_plus_one = landlock_file(fown->file)->fown_layer + 1,
#endif /* CONFIG_AUDIT */
});
return -EPERM;
}
static struct security_hook_list landlock_hooks[] __ro_after_init = {

View File

@ -189,16 +189,13 @@ static inline void print_ipv4_addr(struct audit_buffer *ab, __be32 addr,
}
/**
* dump_common_audit_data - helper to dump common audit data
* audit_log_lsm_data - helper to log common LSM audit data
* @ab : the audit buffer
* @a : common audit data
*
*/
static void dump_common_audit_data(struct audit_buffer *ab,
struct common_audit_data *a)
void audit_log_lsm_data(struct audit_buffer *ab,
const struct common_audit_data *a)
{
char comm[sizeof(current->comm)];
/*
* To keep stack sizes in check force programmers to notice if they
* start making this union too large! See struct lsm_network_audit
@ -206,9 +203,6 @@ static void dump_common_audit_data(struct audit_buffer *ab,
*/
BUILD_BUG_ON(sizeof(a->u) > sizeof(void *)*2);
audit_log_format(ab, " pid=%d comm=", task_tgid_nr(current));
audit_log_untrustedstring(ab, get_task_comm(comm, current));
switch (a->type) {
case LSM_AUDIT_DATA_NONE:
return;
@ -431,6 +425,21 @@ static void dump_common_audit_data(struct audit_buffer *ab,
} /* switch (a->type) */
}
/**
* dump_common_audit_data - helper to dump common audit data
* @ab : the audit buffer
* @a : common audit data
*/
static void dump_common_audit_data(struct audit_buffer *ab,
const struct common_audit_data *a)
{
char comm[sizeof(current->comm)];
audit_log_format(ab, " pid=%d comm=", task_tgid_nr(current));
audit_log_untrustedstring(ab, get_task_comm(comm, current));
audit_log_lsm_data(ab, a);
}
/**
* common_lsm_audit - generic LSM auditing function
* @a: auxiliary audit data

View File

@ -41,6 +41,8 @@ CONFIG_DAMON_PADDR=y
CONFIG_REGMAP_BUILD=y
CONFIG_AUDIT=y
CONFIG_SECURITY=y
CONFIG_SECURITY_APPARMOR=y
CONFIG_SECURITY_LANDLOCK=y

View File

@ -2,3 +2,4 @@
/sandbox-and-launch
/true
/wait-pipe
/wait-pipe-sandbox

View File

@ -10,7 +10,11 @@ src_test := $(wildcard *_test.c)
TEST_GEN_PROGS := $(src_test:.c=)
TEST_GEN_PROGS_EXTENDED := true sandbox-and-launch wait-pipe
TEST_GEN_PROGS_EXTENDED := \
true \
sandbox-and-launch \
wait-pipe \
wait-pipe-sandbox
# Short targets:
$(TEST_GEN_PROGS): LDLIBS += -lcap -lpthread

View File

@ -0,0 +1,472 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Landlock audit helpers
*
* Copyright © 2024-2025 Microsoft Corporation
*/
#define _GNU_SOURCE
#include <errno.h>
#include <linux/audit.h>
#include <linux/limits.h>
#include <linux/netlink.h>
#include <regex.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>
#ifndef ARRAY_SIZE
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
#endif
#ifndef __maybe_unused
#define __maybe_unused __attribute__((__unused__))
#endif
#define REGEX_LANDLOCK_PREFIX "^audit([0-9.:]\\+): domain=\\([0-9a-f]\\+\\)"
struct audit_filter {
__u32 record_type;
size_t exe_len;
char exe[PATH_MAX];
};
struct audit_message {
struct nlmsghdr header;
union {
struct audit_status status;
struct audit_features features;
struct audit_rule_data rule;
struct nlmsgerr err;
char data[PATH_MAX + 200];
};
};
static const struct timeval audit_tv_dom_drop = {
/*
* Because domain deallocation is tied to asynchronous credential
* freeing, receiving such event may take some time. In practice,
* on a small VM, it should not exceed 100k usec, but let's wait up
* to 1 second to be safe.
*/
.tv_sec = 1,
};
static const struct timeval audit_tv_default = {
.tv_usec = 1,
};
static int audit_send(const int fd, const struct audit_message *const msg)
{
struct sockaddr_nl addr = {
.nl_family = AF_NETLINK,
};
int ret;
do {
ret = sendto(fd, msg, msg->header.nlmsg_len, 0,
(struct sockaddr *)&addr, sizeof(addr));
} while (ret < 0 && errno == EINTR);
if (ret < 0)
return -errno;
if (ret != msg->header.nlmsg_len)
return -E2BIG;
return 0;
}
static int audit_recv(const int fd, struct audit_message *msg)
{
struct sockaddr_nl addr;
socklen_t addrlen = sizeof(addr);
struct audit_message msg_tmp;
int err;
if (!msg)
msg = &msg_tmp;
do {
err = recvfrom(fd, msg, sizeof(*msg), 0,
(struct sockaddr *)&addr, &addrlen);
} while (err < 0 && errno == EINTR);
if (err < 0)
return -errno;
if (addrlen != sizeof(addr) || addr.nl_pid != 0)
return -EINVAL;
/* Checks Netlink error or end of messages. */
if (msg->header.nlmsg_type == NLMSG_ERROR)
return msg->err.error;
return 0;
}
static int audit_request(const int fd,
const struct audit_message *const request,
struct audit_message *reply)
{
struct audit_message msg_tmp;
bool first_reply = true;
int err;
err = audit_send(fd, request);
if (err)
return err;
if (!reply)
reply = &msg_tmp;
do {
if (first_reply)
first_reply = false;
else
reply = &msg_tmp;
err = audit_recv(fd, reply);
if (err)
return err;
} while (reply->header.nlmsg_type != NLMSG_ERROR &&
reply->err.msg.nlmsg_type != request->header.nlmsg_type);
return reply->err.error;
}
static int audit_filter_exe(const int audit_fd,
const struct audit_filter *const filter,
const __u16 type)
{
struct audit_message msg = {
.header = {
.nlmsg_len = NLMSG_SPACE(sizeof(msg.rule)) +
NLMSG_ALIGN(filter->exe_len),
.nlmsg_type = type,
.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK,
},
.rule = {
.flags = AUDIT_FILTER_EXCLUDE,
.action = AUDIT_NEVER,
.field_count = 1,
.fields[0] = filter->record_type,
.fieldflags[0] = AUDIT_NOT_EQUAL,
.values[0] = filter->exe_len,
.buflen = filter->exe_len,
}
};
if (filter->record_type != AUDIT_EXE)
return -EINVAL;
memcpy(msg.rule.buf, filter->exe, filter->exe_len);
return audit_request(audit_fd, &msg, NULL);
}
static int audit_filter_drop(const int audit_fd, const __u16 type)
{
struct audit_message msg = {
.header = {
.nlmsg_len = NLMSG_SPACE(sizeof(msg.rule)),
.nlmsg_type = type,
.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK,
},
.rule = {
.flags = AUDIT_FILTER_EXCLUDE,
.action = AUDIT_NEVER,
.field_count = 1,
.fields[0] = AUDIT_MSGTYPE,
.fieldflags[0] = AUDIT_NOT_EQUAL,
.values[0] = AUDIT_LANDLOCK_DOMAIN,
}
};
return audit_request(audit_fd, &msg, NULL);
}
static int audit_set_status(int fd, __u32 key, __u32 val)
{
const struct audit_message msg = {
.header = {
.nlmsg_len = NLMSG_SPACE(sizeof(msg.status)),
.nlmsg_type = AUDIT_SET,
.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK,
},
.status = {
.mask = key,
.enabled = key == AUDIT_STATUS_ENABLED ? val : 0,
.pid = key == AUDIT_STATUS_PID ? val : 0,
}
};
return audit_request(fd, &msg, NULL);
}
/* Returns a pointer to the last filled character of @dst, which is `\0`. */
static __maybe_unused char *regex_escape(const char *const src, char *dst,
size_t dst_size)
{
char *d = dst;
for (const char *s = src; *s; s++) {
switch (*s) {
case '$':
case '*':
case '.':
case '[':
case '\\':
case ']':
case '^':
if (d >= dst + dst_size - 2)
return (char *)-ENOMEM;
*d++ = '\\';
*d++ = *s;
break;
default:
if (d >= dst + dst_size - 1)
return (char *)-ENOMEM;
*d++ = *s;
}
}
if (d >= dst + dst_size - 1)
return (char *)-ENOMEM;
*d = '\0';
return d;
}
/*
* @domain_id: The domain ID extracted from the audit message (if the first part
* of @pattern is REGEX_LANDLOCK_PREFIX). It is set to 0 if the domain ID is
* not found.
*/
static int audit_match_record(int audit_fd, const __u16 type,
const char *const pattern, __u64 *domain_id)
{
struct audit_message msg;
int ret, err = 0;
bool matches_record = !type;
regmatch_t matches[2];
regex_t regex;
ret = regcomp(&regex, pattern, 0);
if (ret)
return -EINVAL;
do {
memset(&msg, 0, sizeof(msg));
err = audit_recv(audit_fd, &msg);
if (err)
goto out;
if (msg.header.nlmsg_type == type)
matches_record = true;
} while (!matches_record);
ret = regexec(&regex, msg.data, ARRAY_SIZE(matches), matches, 0);
if (ret) {
printf("DATA: %s\n", msg.data);
printf("ERROR: no match for pattern: %s\n", pattern);
err = -ENOENT;
}
if (domain_id) {
*domain_id = 0;
if (matches[1].rm_so != -1) {
int match_len = matches[1].rm_eo - matches[1].rm_so;
/* The maximal characters of a 2^64 hexadecimal number is 17. */
char dom_id[18];
if (match_len > 0 && match_len < sizeof(dom_id)) {
memcpy(dom_id, msg.data + matches[1].rm_so,
match_len);
dom_id[match_len] = '\0';
if (domain_id)
*domain_id = strtoull(dom_id, NULL, 16);
}
}
}
out:
regfree(&regex);
return err;
}
static int __maybe_unused matches_log_domain_allocated(int audit_fd,
__u64 *domain_id)
{
return audit_match_record(
audit_fd, AUDIT_LANDLOCK_DOMAIN,
REGEX_LANDLOCK_PREFIX
" status=allocated mode=enforcing pid=[0-9]\\+ uid=[0-9]\\+"
" exe=\"[^\"]\\+\" comm=\".*_test\"$",
domain_id);
}
static int __maybe_unused matches_log_domain_deallocated(
int audit_fd, unsigned int num_denials, __u64 *domain_id)
{
static const char log_template[] = REGEX_LANDLOCK_PREFIX
" status=deallocated denials=%u$";
char log_match[sizeof(log_template) + 10];
int log_match_len;
log_match_len = snprintf(log_match, sizeof(log_match), log_template,
num_denials);
if (log_match_len > sizeof(log_match))
return -E2BIG;
return audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match,
domain_id);
}
struct audit_records {
size_t access;
size_t domain;
};
static int audit_count_records(int audit_fd, struct audit_records *records)
{
struct audit_message msg;
int err;
records->access = 0;
records->domain = 0;
do {
memset(&msg, 0, sizeof(msg));
err = audit_recv(audit_fd, &msg);
if (err) {
if (err == -EAGAIN)
return 0;
else
return err;
}
switch (msg.header.nlmsg_type) {
case AUDIT_LANDLOCK_ACCESS:
records->access++;
break;
case AUDIT_LANDLOCK_DOMAIN:
records->domain++;
break;
}
} while (true);
return 0;
}
static int audit_init(void)
{
int fd, err;
fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
if (fd < 0)
return -errno;
err = audit_set_status(fd, AUDIT_STATUS_ENABLED, 1);
if (err)
return err;
err = audit_set_status(fd, AUDIT_STATUS_PID, getpid());
if (err)
return err;
/* Sets a timeout for negative tests. */
err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default,
sizeof(audit_tv_default));
if (err)
return -errno;
return fd;
}
static int audit_init_filter_exe(struct audit_filter *filter, const char *path)
{
char *absolute_path = NULL;
/* It is assume that there is not already filtering rules. */
filter->record_type = AUDIT_EXE;
if (!path) {
filter->exe_len = readlink("/proc/self/exe", filter->exe,
sizeof(filter->exe) - 1);
if (filter->exe_len < 0)
return -errno;
return 0;
}
absolute_path = realpath(path, NULL);
if (!absolute_path)
return -errno;
/* No need for the terminating NULL byte. */
filter->exe_len = strlen(absolute_path);
if (filter->exe_len > sizeof(filter->exe))
return -E2BIG;
memcpy(filter->exe, absolute_path, filter->exe_len);
free(absolute_path);
return 0;
}
static int audit_cleanup(int audit_fd, struct audit_filter *filter)
{
struct audit_filter new_filter;
if (audit_fd < 0 || !filter) {
int err;
/*
* Simulates audit_init_with_exe_filter() when called from
* FIXTURE_TEARDOWN_PARENT().
*/
audit_fd = audit_init();
if (audit_fd < 0)
return audit_fd;
filter = &new_filter;
err = audit_init_filter_exe(filter, NULL);
if (err)
return err;
}
/* Filters might not be in place. */
audit_filter_exe(audit_fd, filter, AUDIT_DEL_RULE);
audit_filter_drop(audit_fd, AUDIT_DEL_RULE);
/*
* Because audit_cleanup() might not be called by the test auditd
* process, it might not be possible to explicitly set it. Anyway,
* AUDIT_STATUS_ENABLED will implicitly be set to 0 when the auditd
* process will exit.
*/
return close(audit_fd);
}
static int audit_init_with_exe_filter(struct audit_filter *filter)
{
int fd, err;
fd = audit_init();
if (fd < 0)
return fd;
err = audit_init_filter_exe(filter, NULL);
if (err)
return err;
err = audit_filter_exe(fd, filter, AUDIT_ADD_RULE);
if (err)
return err;
return fd;
}

View File

@ -0,0 +1,551 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Landlock tests - Audit
*
* Copyright © 2024-2025 Microsoft Corporation
*/
#define _GNU_SOURCE
#include <errno.h>
#include <limits.h>
#include <linux/landlock.h>
#include <stdlib.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "audit.h"
#include "common.h"
static int matches_log_signal(struct __test_metadata *const _metadata,
int audit_fd, const pid_t opid, __u64 *domain_id)
{
static const char log_template[] = REGEX_LANDLOCK_PREFIX
" blockers=scope\\.signal opid=%d ocomm=\"audit_test\"$";
char log_match[sizeof(log_template) + 10];
int log_match_len;
log_match_len =
snprintf(log_match, sizeof(log_match), log_template, opid);
if (log_match_len > sizeof(log_match))
return -E2BIG;
return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match,
domain_id);
}
FIXTURE(audit)
{
struct audit_filter audit_filter;
int audit_fd;
__u64(*domain_stack)[16];
};
FIXTURE_SETUP(audit)
{
disable_caps(_metadata);
set_cap(_metadata, CAP_AUDIT_CONTROL);
self->audit_fd = audit_init_with_exe_filter(&self->audit_filter);
EXPECT_LE(0, self->audit_fd)
{
const char *error_msg;
/* kill "$(auditctl -s | sed -ne 's/^pid \([0-9]\+\)$/\1/p')" */
if (self->audit_fd == -EEXIST)
error_msg = "socket already in use (e.g. auditd)";
else
error_msg = strerror(-self->audit_fd);
TH_LOG("Failed to initialize audit: %s", error_msg);
}
clear_cap(_metadata, CAP_AUDIT_CONTROL);
self->domain_stack = mmap(NULL, sizeof(*self->domain_stack),
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
ASSERT_NE(MAP_FAILED, self->domain_stack);
memset(self->domain_stack, 0, sizeof(*self->domain_stack));
}
FIXTURE_TEARDOWN(audit)
{
EXPECT_EQ(0, munmap(self->domain_stack, sizeof(*self->domain_stack)));
set_cap(_metadata, CAP_AUDIT_CONTROL);
EXPECT_EQ(0, audit_cleanup(self->audit_fd, &self->audit_filter));
clear_cap(_metadata, CAP_AUDIT_CONTROL);
}
TEST_F(audit, layers)
{
const struct landlock_ruleset_attr ruleset_attr = {
.scoped = LANDLOCK_SCOPE_SIGNAL,
};
int status, ruleset_fd, i;
__u64 prev_dom = 3;
pid_t child;
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
for (i = 0; i < ARRAY_SIZE(*self->domain_stack); i++) {
__u64 denial_dom = 1;
__u64 allocated_dom = 2;
EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
/* Creates a denial to get the domain ID. */
EXPECT_EQ(-1, kill(getppid(), 0));
EXPECT_EQ(EPERM, errno);
EXPECT_EQ(0,
matches_log_signal(_metadata, self->audit_fd,
getppid(), &denial_dom));
EXPECT_EQ(0, matches_log_domain_allocated(
self->audit_fd, &allocated_dom));
EXPECT_NE(denial_dom, 1);
EXPECT_NE(denial_dom, 0);
EXPECT_EQ(denial_dom, allocated_dom);
/* Checks that the new domain is younger than the previous one. */
EXPECT_GT(allocated_dom, prev_dom);
prev_dom = allocated_dom;
(*self->domain_stack)[i] = allocated_dom;
}
/* Checks that we reached the maximum number of layers. */
EXPECT_EQ(-1, landlock_restrict_self(ruleset_fd, 0));
EXPECT_EQ(E2BIG, errno);
/* Updates filter rules to match the drop record. */
set_cap(_metadata, CAP_AUDIT_CONTROL);
EXPECT_EQ(0, audit_filter_drop(self->audit_fd, AUDIT_ADD_RULE));
EXPECT_EQ(0,
audit_filter_exe(self->audit_fd, &self->audit_filter,
AUDIT_DEL_RULE));
clear_cap(_metadata, CAP_AUDIT_CONTROL);
_exit(_metadata->exit_code);
return;
}
ASSERT_EQ(child, waitpid(child, &status, 0));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
/* Purges log from deallocated domains. */
EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
&audit_tv_dom_drop, sizeof(audit_tv_dom_drop)));
for (i = ARRAY_SIZE(*self->domain_stack) - 1; i >= 0; i--) {
__u64 deallocated_dom = 2;
EXPECT_EQ(0, matches_log_domain_deallocated(self->audit_fd, 1,
&deallocated_dom));
EXPECT_EQ((*self->domain_stack)[i], deallocated_dom)
{
TH_LOG("Failed to match domain %llx (#%d)",
(*self->domain_stack)[i], i);
}
}
EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
&audit_tv_default, sizeof(audit_tv_default)));
EXPECT_EQ(0, close(ruleset_fd));
}
FIXTURE(audit_flags)
{
struct audit_filter audit_filter;
int audit_fd;
__u64 *domain_id;
};
FIXTURE_VARIANT(audit_flags)
{
const int restrict_flags;
};
/* clang-format off */
FIXTURE_VARIANT_ADD(audit_flags, default) {
/* clang-format on */
.restrict_flags = 0,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(audit_flags, same_exec_off) {
/* clang-format on */
.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(audit_flags, subdomains_off) {
/* clang-format on */
.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(audit_flags, cross_exec_on) {
/* clang-format on */
.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON,
};
FIXTURE_SETUP(audit_flags)
{
disable_caps(_metadata);
set_cap(_metadata, CAP_AUDIT_CONTROL);
self->audit_fd = audit_init_with_exe_filter(&self->audit_filter);
EXPECT_LE(0, self->audit_fd)
{
const char *error_msg;
/* kill "$(auditctl -s | sed -ne 's/^pid \([0-9]\+\)$/\1/p')" */
if (self->audit_fd == -EEXIST)
error_msg = "socket already in use (e.g. auditd)";
else
error_msg = strerror(-self->audit_fd);
TH_LOG("Failed to initialize audit: %s", error_msg);
}
clear_cap(_metadata, CAP_AUDIT_CONTROL);
self->domain_id = mmap(NULL, sizeof(*self->domain_id),
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
ASSERT_NE(MAP_FAILED, self->domain_id);
/* Domain IDs are greater or equal to 2^32. */
*self->domain_id = 1;
}
FIXTURE_TEARDOWN(audit_flags)
{
EXPECT_EQ(0, munmap(self->domain_id, sizeof(*self->domain_id)));
set_cap(_metadata, CAP_AUDIT_CONTROL);
EXPECT_EQ(0, audit_cleanup(self->audit_fd, &self->audit_filter));
clear_cap(_metadata, CAP_AUDIT_CONTROL);
}
TEST_F(audit_flags, signal)
{
int status;
pid_t child;
struct audit_records records;
__u64 deallocated_dom = 2;
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
const struct landlock_ruleset_attr ruleset_attr = {
.scoped = LANDLOCK_SCOPE_SIGNAL,
};
int ruleset_fd;
/* Add filesystem restrictions. */
ruleset_fd = landlock_create_ruleset(&ruleset_attr,
sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
ASSERT_EQ(0, landlock_restrict_self(ruleset_fd,
variant->restrict_flags));
EXPECT_EQ(0, close(ruleset_fd));
/* First signal checks to test log entries. */
EXPECT_EQ(-1, kill(getppid(), 0));
EXPECT_EQ(EPERM, errno);
if (variant->restrict_flags &
LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) {
EXPECT_EQ(-EAGAIN, matches_log_signal(
_metadata, self->audit_fd,
getppid(), self->domain_id));
EXPECT_EQ(*self->domain_id, 1);
} else {
__u64 allocated_dom = 3;
EXPECT_EQ(0, matches_log_signal(
_metadata, self->audit_fd,
getppid(), self->domain_id));
/* Checks domain information records. */
EXPECT_EQ(0, matches_log_domain_allocated(
self->audit_fd, &allocated_dom));
EXPECT_NE(*self->domain_id, 1);
EXPECT_NE(*self->domain_id, 0);
EXPECT_EQ(*self->domain_id, allocated_dom);
}
/* Second signal checks to test audit_count_records(). */
EXPECT_EQ(-1, kill(getppid(), 0));
EXPECT_EQ(EPERM, errno);
/* Makes sure there is no superfluous logged records. */
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
if (variant->restrict_flags &
LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) {
EXPECT_EQ(0, records.access);
} else {
EXPECT_EQ(1, records.access);
}
EXPECT_EQ(0, records.domain);
/* Updates filter rules to match the drop record. */
set_cap(_metadata, CAP_AUDIT_CONTROL);
EXPECT_EQ(0, audit_filter_drop(self->audit_fd, AUDIT_ADD_RULE));
EXPECT_EQ(0,
audit_filter_exe(self->audit_fd, &self->audit_filter,
AUDIT_DEL_RULE));
clear_cap(_metadata, CAP_AUDIT_CONTROL);
_exit(_metadata->exit_code);
return;
}
ASSERT_EQ(child, waitpid(child, &status, 0));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
if (variant->restrict_flags &
LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) {
EXPECT_EQ(-EAGAIN,
matches_log_domain_deallocated(self->audit_fd, 0,
&deallocated_dom));
EXPECT_EQ(deallocated_dom, 2);
} else {
EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
&audit_tv_dom_drop,
sizeof(audit_tv_dom_drop)));
EXPECT_EQ(0, matches_log_domain_deallocated(self->audit_fd, 2,
&deallocated_dom));
EXPECT_NE(deallocated_dom, 2);
EXPECT_NE(deallocated_dom, 0);
EXPECT_EQ(deallocated_dom, *self->domain_id);
EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
&audit_tv_default,
sizeof(audit_tv_default)));
}
}
static int matches_log_fs_read_root(int audit_fd)
{
return audit_match_record(
audit_fd, AUDIT_LANDLOCK_ACCESS,
REGEX_LANDLOCK_PREFIX
" blockers=fs\\.read_dir path=\"/\" dev=\"[^\"]\\+\" ino=[0-9]\\+$",
NULL);
}
FIXTURE(audit_exec)
{
struct audit_filter audit_filter;
int audit_fd;
};
FIXTURE_VARIANT(audit_exec)
{
const int restrict_flags;
};
/* clang-format off */
FIXTURE_VARIANT_ADD(audit_exec, default) {
/* clang-format on */
.restrict_flags = 0,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(audit_exec, same_exec_off) {
/* clang-format on */
.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(audit_exec, subdomains_off) {
/* clang-format on */
.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(audit_exec, cross_exec_on) {
/* clang-format on */
.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(audit_exec, subdomains_off_and_cross_exec_on) {
/* clang-format on */
.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON,
};
FIXTURE_SETUP(audit_exec)
{
disable_caps(_metadata);
set_cap(_metadata, CAP_AUDIT_CONTROL);
self->audit_fd = audit_init();
EXPECT_LE(0, self->audit_fd)
{
const char *error_msg;
/* kill "$(auditctl -s | sed -ne 's/^pid \([0-9]\+\)$/\1/p')" */
if (self->audit_fd == -EEXIST)
error_msg = "socket already in use (e.g. auditd)";
else
error_msg = strerror(-self->audit_fd);
TH_LOG("Failed to initialize audit: %s", error_msg);
}
/* Applies test filter for the bin_wait_pipe_sandbox program. */
EXPECT_EQ(0, audit_init_filter_exe(&self->audit_filter,
bin_wait_pipe_sandbox));
EXPECT_EQ(0, audit_filter_exe(self->audit_fd, &self->audit_filter,
AUDIT_ADD_RULE));
clear_cap(_metadata, CAP_AUDIT_CONTROL);
}
FIXTURE_TEARDOWN(audit_exec)
{
set_cap(_metadata, CAP_AUDIT_CONTROL);
EXPECT_EQ(0, audit_filter_exe(self->audit_fd, &self->audit_filter,
AUDIT_DEL_RULE));
clear_cap(_metadata, CAP_AUDIT_CONTROL);
EXPECT_EQ(0, close(self->audit_fd));
}
TEST_F(audit_exec, signal_and_open)
{
struct audit_records records;
int pipe_child[2], pipe_parent[2];
char buf_parent;
pid_t child;
int status;
ASSERT_EQ(0, pipe2(pipe_child, 0));
ASSERT_EQ(0, pipe2(pipe_parent, 0));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
const struct landlock_ruleset_attr layer1 = {
.scoped = LANDLOCK_SCOPE_SIGNAL,
};
char pipe_child_str[12], pipe_parent_str[12];
char *const argv[] = { (char *)bin_wait_pipe_sandbox,
pipe_child_str, pipe_parent_str, NULL };
int ruleset_fd;
/* Passes the pipe FDs to the executed binary. */
EXPECT_EQ(0, close(pipe_child[0]));
EXPECT_EQ(0, close(pipe_parent[1]));
snprintf(pipe_child_str, sizeof(pipe_child_str), "%d",
pipe_child[1]);
snprintf(pipe_parent_str, sizeof(pipe_parent_str), "%d",
pipe_parent[0]);
ruleset_fd =
landlock_create_ruleset(&layer1, sizeof(layer1), 0);
if (ruleset_fd < 0) {
perror("Failed to create a ruleset");
_exit(1);
}
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (landlock_restrict_self(ruleset_fd,
variant->restrict_flags)) {
perror("Failed to restrict self");
_exit(1);
}
close(ruleset_fd);
ASSERT_EQ(0, execve(argv[0], argv, NULL))
{
TH_LOG("Failed to execute \"%s\": %s", argv[0],
strerror(errno));
};
_exit(1);
return;
}
EXPECT_EQ(0, close(pipe_child[1]));
EXPECT_EQ(0, close(pipe_parent[0]));
/* Waits for the child. */
EXPECT_EQ(1, read(pipe_child[0], &buf_parent, 1));
/* Tests that there was no denial until now. */
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(0, records.domain);
/*
* Wait for the child to do a first denied action by layer1 and
* sandbox itself with layer2.
*/
EXPECT_EQ(1, write(pipe_parent[1], ".", 1));
EXPECT_EQ(1, read(pipe_child[0], &buf_parent, 1));
/* Tests that the audit record only matches the child. */
if (variant->restrict_flags & LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON) {
/* Matches the current domain. */
EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd,
getpid(), NULL));
}
/* Checks that we didn't miss anything. */
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
/*
* Wait for the child to do a second denied action by layer1 and
* layer2, and sandbox itself with layer3.
*/
EXPECT_EQ(1, write(pipe_parent[1], ".", 1));
EXPECT_EQ(1, read(pipe_child[0], &buf_parent, 1));
/* Tests that the audit record only matches the child. */
if (variant->restrict_flags & LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON) {
/* Matches the current domain. */
EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd,
getpid(), NULL));
}
if (!(variant->restrict_flags &
LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
/* Matches the child domain. */
EXPECT_EQ(0, matches_log_fs_read_root(self->audit_fd));
}
/* Checks that we didn't miss anything. */
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
/* Waits for the child to terminate. */
EXPECT_EQ(1, write(pipe_parent[1], ".", 1));
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_EQ(1, WIFEXITED(status));
ASSERT_EQ(0, WEXITSTATUS(status));
/* Tests that the audit record only matches the child. */
if (!(variant->restrict_flags &
LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
/*
* Matches the child domains, which tests that the
* llcred->domain_exec bitmask is correctly updated with a new
* domain.
*/
EXPECT_EQ(0, matches_log_fs_read_root(self->audit_fd));
EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd,
getpid(), NULL));
}
/* Checks that we didn't miss anything. */
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
}
TEST_HARNESS_MAIN

View File

@ -76,7 +76,7 @@ TEST(abi_version)
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
};
ASSERT_EQ(6, landlock_create_ruleset(NULL, 0,
ASSERT_EQ(7, landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION));
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
@ -98,10 +98,54 @@ TEST(abi_version)
ASSERT_EQ(EINVAL, errno);
}
/*
* Old source trees might not have the set of Kselftest fixes related to kernel
* UAPI headers.
*/
#ifndef LANDLOCK_CREATE_RULESET_ERRATA
#define LANDLOCK_CREATE_RULESET_ERRATA (1U << 1)
#endif
TEST(errata)
{
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
};
int errata;
errata = landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_ERRATA);
/* The errata bitmask will not be backported to tests. */
ASSERT_LE(0, errata);
TH_LOG("errata: 0x%x", errata);
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
LANDLOCK_CREATE_RULESET_ERRATA));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_create_ruleset(NULL, sizeof(ruleset_attr),
LANDLOCK_CREATE_RULESET_ERRATA));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1,
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr),
LANDLOCK_CREATE_RULESET_ERRATA));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_create_ruleset(
NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION |
LANDLOCK_CREATE_RULESET_ERRATA));
ASSERT_EQ(-1, landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_ERRATA |
1 << 31));
ASSERT_EQ(EINVAL, errno);
}
/* Tests ordering of syscall argument checks. */
TEST(create_ruleset_checks_ordering)
{
const int last_flag = LANDLOCK_CREATE_RULESET_VERSION;
const int last_flag = LANDLOCK_CREATE_RULESET_ERRATA;
const int invalid_flag = last_flag << 1;
int ruleset_fd;
const struct landlock_ruleset_attr ruleset_attr = {
@ -233,6 +277,88 @@ TEST(restrict_self_checks_ordering)
ASSERT_EQ(0, close(ruleset_fd));
}
TEST(restrict_self_fd)
{
int fd;
fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
ASSERT_LE(0, fd);
EXPECT_EQ(-1, landlock_restrict_self(fd, 0));
EXPECT_EQ(EBADFD, errno);
}
TEST(restrict_self_fd_flags)
{
int fd;
fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
ASSERT_LE(0, fd);
/*
* LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF accepts -1 but not any file
* descriptor.
*/
EXPECT_EQ(-1, landlock_restrict_self(
fd, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
EXPECT_EQ(EBADFD, errno);
}
TEST(restrict_self_flags)
{
const __u32 last_flag = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF;
/* Tests invalid flag combinations. */
EXPECT_EQ(-1, landlock_restrict_self(-1, last_flag << 1));
EXPECT_EQ(EINVAL, errno);
EXPECT_EQ(-1, landlock_restrict_self(-1, -1));
EXPECT_EQ(EINVAL, errno);
/* Tests valid flag combinations. */
EXPECT_EQ(-1, landlock_restrict_self(-1, 0));
EXPECT_EQ(EBADF, errno);
EXPECT_EQ(-1, landlock_restrict_self(
-1, LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF));
EXPECT_EQ(EBADF, errno);
EXPECT_EQ(-1,
landlock_restrict_self(
-1,
LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
EXPECT_EQ(EBADF, errno);
EXPECT_EQ(-1,
landlock_restrict_self(
-1,
LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
EXPECT_EQ(EBADF, errno);
EXPECT_EQ(-1, landlock_restrict_self(
-1, LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON));
EXPECT_EQ(EBADF, errno);
EXPECT_EQ(-1,
landlock_restrict_self(
-1, LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON));
EXPECT_EQ(EBADF, errno);
/* Tests with an invalid ruleset_fd. */
EXPECT_EQ(-1, landlock_restrict_self(
-2, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
EXPECT_EQ(EBADF, errno);
EXPECT_EQ(0, landlock_restrict_self(
-1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
}
TEST(ruleset_fd_io)
{
struct landlock_ruleset_attr ruleset_attr = {

View File

@ -11,6 +11,7 @@
#include <errno.h>
#include <linux/securebits.h>
#include <sys/capability.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/wait.h>
@ -30,6 +31,7 @@
static const char bin_sandbox_and_launch[] = "./sandbox-and-launch";
static const char bin_wait_pipe[] = "./wait-pipe";
static const char bin_wait_pipe_sandbox[] = "./wait-pipe-sandbox";
static void _init_caps(struct __test_metadata *const _metadata, bool drop_all)
{
@ -37,10 +39,12 @@ static void _init_caps(struct __test_metadata *const _metadata, bool drop_all)
/* Only these three capabilities are useful for the tests. */
const cap_value_t caps[] = {
/* clang-format off */
CAP_AUDIT_CONTROL,
CAP_DAC_OVERRIDE,
CAP_MKNOD,
CAP_NET_ADMIN,
CAP_NET_BIND_SERVICE,
CAP_SETUID,
CAP_SYS_ADMIN,
CAP_SYS_CHROOT,
/* clang-format on */
@ -204,6 +208,22 @@ enforce_ruleset(struct __test_metadata *const _metadata, const int ruleset_fd)
}
}
static void __maybe_unused
drop_access_rights(struct __test_metadata *const _metadata,
const struct landlock_ruleset_attr *const ruleset_attr)
{
int ruleset_fd;
ruleset_fd =
landlock_create_ruleset(ruleset_attr, sizeof(*ruleset_attr), 0);
EXPECT_LE(0, ruleset_fd)
{
TH_LOG("Failed to create a ruleset: %s", strerror(errno));
}
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
}
struct protocol_variant {
int domain;
int type;

View File

@ -1,4 +1,5 @@
CONFIG_AF_UNIX_OOB=y
CONFIG_AUDIT=y
CONFIG_CGROUPS=y
CONFIG_CGROUP_SCHED=y
CONFIG_INET=y

View File

@ -41,6 +41,7 @@
#define _ASM_GENERIC_FCNTL_H
#include <linux/fcntl.h>
#include "audit.h"
#include "common.h"
#ifndef renameat2
@ -5554,4 +5555,597 @@ TEST_F_FORK(layout3_fs, release_inodes)
ASSERT_EQ(EACCES, test_open(TMP_DIR, O_RDONLY));
}
static int matches_log_fs_extra(struct __test_metadata *const _metadata,
int audit_fd, const char *const blockers,
const char *const path, const char *const extra)
{
static const char log_template[] = REGEX_LANDLOCK_PREFIX
" blockers=fs\\.%s path=\"%s\" dev=\"[^\"]\\+\" ino=[0-9]\\+$";
char *absolute_path = NULL;
size_t log_match_remaining = sizeof(log_template) + strlen(blockers) +
PATH_MAX * 2 +
(extra ? strlen(extra) : 0) + 1;
char log_match[log_match_remaining];
char *log_match_cursor = log_match;
size_t chunk_len;
chunk_len = snprintf(log_match_cursor, log_match_remaining,
REGEX_LANDLOCK_PREFIX " blockers=%s path=\"",
blockers);
if (chunk_len < 0 || chunk_len >= log_match_remaining)
return -E2BIG;
/*
* It is assume that absolute_path does not contain control characters nor
* spaces, see audit_string_contains_control().
*/
absolute_path = realpath(path, NULL);
if (!absolute_path)
return -errno;
log_match_remaining -= chunk_len;
log_match_cursor += chunk_len;
log_match_cursor = regex_escape(absolute_path, log_match_cursor,
log_match_remaining);
free(absolute_path);
if (log_match_cursor < 0)
return (long long)log_match_cursor;
log_match_remaining -= log_match_cursor - log_match;
chunk_len = snprintf(log_match_cursor, log_match_remaining,
"\" dev=\"[^\"]\\+\" ino=[0-9]\\+%s$",
extra ?: "");
if (chunk_len < 0 || chunk_len >= log_match_remaining)
return -E2BIG;
return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match,
NULL);
}
static int matches_log_fs(struct __test_metadata *const _metadata, int audit_fd,
const char *const blockers, const char *const path)
{
return matches_log_fs_extra(_metadata, audit_fd, blockers, path, NULL);
}
FIXTURE(audit_layout1)
{
struct audit_filter audit_filter;
int audit_fd;
};
FIXTURE_SETUP(audit_layout1)
{
prepare_layout(_metadata);
create_layout1(_metadata);
set_cap(_metadata, CAP_AUDIT_CONTROL);
self->audit_fd = audit_init_with_exe_filter(&self->audit_filter);
EXPECT_LE(0, self->audit_fd);
disable_caps(_metadata);
}
FIXTURE_TEARDOWN_PARENT(audit_layout1)
{
remove_layout1(_metadata);
cleanup_layout(_metadata);
EXPECT_EQ(0, audit_cleanup(-1, NULL));
}
TEST_F(audit_layout1, execute_make)
{
struct audit_records records;
copy_file(_metadata, bin_true, file1_s1d1);
test_execute(_metadata, 0, file1_s1d1);
test_check_exec(_metadata, 0, file1_s1d1);
drop_access_rights(_metadata,
&(struct landlock_ruleset_attr){
.handled_access_fs =
LANDLOCK_ACCESS_FS_EXECUTE,
});
test_execute(_metadata, EACCES, file1_s1d1);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.execute",
file1_s1d1));
test_check_exec(_metadata, EACCES, file1_s1d1);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.execute",
file1_s1d1));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(0, records.domain);
}
/*
* Using a set of handled/denied access rights make it possible to check that
* only the blocked ones are logged.
*/
/* clang-format off */
static const __u64 access_fs_16 =
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;
/* clang-format on */
TEST_F(audit_layout1, execute_read)
{
struct audit_records records;
copy_file(_metadata, bin_true, file1_s1d1);
test_execute(_metadata, 0, file1_s1d1);
test_check_exec(_metadata, 0, file1_s1d1);
drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
.handled_access_fs = access_fs_16,
});
/*
* The only difference with the previous audit_layout1.execute_read test is
* the extra ",fs\\.read_file" blocked by the executable file.
*/
test_execute(_metadata, EACCES, file1_s1d1);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
"fs\\.execute,fs\\.read_file", file1_s1d1));
test_check_exec(_metadata, EACCES, file1_s1d1);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
"fs\\.execute,fs\\.read_file", file1_s1d1));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(0, records.domain);
}
TEST_F(audit_layout1, write_file)
{
struct audit_records records;
drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
.handled_access_fs = access_fs_16,
});
EXPECT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY));
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
"fs\\.write_file", file1_s1d1));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
}
TEST_F(audit_layout1, read_file)
{
struct audit_records records;
drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
.handled_access_fs = access_fs_16,
});
EXPECT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.read_file",
file1_s1d1));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
}
TEST_F(audit_layout1, read_dir)
{
struct audit_records records;
drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
.handled_access_fs = access_fs_16,
});
EXPECT_EQ(EACCES, test_open(dir_s1d1, O_DIRECTORY));
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.read_dir",
dir_s1d1));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
}
TEST_F(audit_layout1, remove_dir)
{
struct audit_records records;
EXPECT_EQ(0, unlink(file1_s1d3));
EXPECT_EQ(0, unlink(file2_s1d3));
drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
.handled_access_fs = access_fs_16,
});
EXPECT_EQ(-1, rmdir(dir_s1d3));
EXPECT_EQ(EACCES, errno);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
"fs\\.remove_dir", dir_s1d2));
EXPECT_EQ(-1, unlinkat(AT_FDCWD, dir_s1d3, AT_REMOVEDIR));
EXPECT_EQ(EACCES, errno);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
"fs\\.remove_dir", dir_s1d2));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(0, records.domain);
}
TEST_F(audit_layout1, remove_file)
{
struct audit_records records;
drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
.handled_access_fs = access_fs_16,
});
EXPECT_EQ(-1, unlink(file1_s1d3));
EXPECT_EQ(EACCES, errno);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
"fs\\.remove_file", dir_s1d3));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
}
TEST_F(audit_layout1, make_char)
{
struct audit_records records;
EXPECT_EQ(0, unlink(file1_s1d3));
drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
.handled_access_fs = access_fs_16,
});
EXPECT_EQ(-1, mknod(file1_s1d3, S_IFCHR | 0644, 0));
EXPECT_EQ(EACCES, errno);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_char",
dir_s1d3));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
}
TEST_F(audit_layout1, make_dir)
{
struct audit_records records;
EXPECT_EQ(0, unlink(file1_s1d3));
drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
.handled_access_fs = access_fs_16,
});
EXPECT_EQ(-1, mkdir(file1_s1d3, 0755));
EXPECT_EQ(EACCES, errno);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_dir",
dir_s1d3));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
}
TEST_F(audit_layout1, make_reg)
{
struct audit_records records;
EXPECT_EQ(0, unlink(file1_s1d3));
drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
.handled_access_fs = access_fs_16,
});
EXPECT_EQ(-1, mknod(file1_s1d3, S_IFREG | 0644, 0));
EXPECT_EQ(EACCES, errno);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_reg",
dir_s1d3));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
}
TEST_F(audit_layout1, make_sock)
{
struct audit_records records;
EXPECT_EQ(0, unlink(file1_s1d3));
drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
.handled_access_fs = access_fs_16,
});
EXPECT_EQ(-1, mknod(file1_s1d3, S_IFSOCK | 0644, 0));
EXPECT_EQ(EACCES, errno);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_sock",
dir_s1d3));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
}
TEST_F(audit_layout1, make_fifo)
{
struct audit_records records;
EXPECT_EQ(0, unlink(file1_s1d3));
drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
.handled_access_fs = access_fs_16,
});
EXPECT_EQ(-1, mknod(file1_s1d3, S_IFIFO | 0644, 0));
EXPECT_EQ(EACCES, errno);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_fifo",
dir_s1d3));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
}
TEST_F(audit_layout1, make_block)
{
struct audit_records records;
EXPECT_EQ(0, unlink(file1_s1d3));
drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
.handled_access_fs = access_fs_16,
});
EXPECT_EQ(-1, mknod(file1_s1d3, S_IFBLK | 0644, 0));
EXPECT_EQ(EACCES, errno);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
"fs\\.make_block", dir_s1d3));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
}
TEST_F(audit_layout1, make_sym)
{
struct audit_records records;
EXPECT_EQ(0, unlink(file1_s1d3));
drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
.handled_access_fs = access_fs_16,
});
EXPECT_EQ(-1, symlink("target", file1_s1d3));
EXPECT_EQ(EACCES, errno);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_sym",
dir_s1d3));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
}
TEST_F(audit_layout1, refer_handled)
{
struct audit_records records;
EXPECT_EQ(0, unlink(file1_s1d3));
drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
.handled_access_fs =
LANDLOCK_ACCESS_FS_REFER,
});
EXPECT_EQ(-1, link(file1_s1d1, file1_s1d3));
EXPECT_EQ(EXDEV, errno);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer",
dir_s1d1));
EXPECT_EQ(0, matches_log_domain_allocated(self->audit_fd, NULL));
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer",
dir_s1d3));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(0, records.domain);
}
TEST_F(audit_layout1, refer_make)
{
struct audit_records records;
EXPECT_EQ(0, unlink(file1_s1d3));
drop_access_rights(_metadata,
&(struct landlock_ruleset_attr){
.handled_access_fs =
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_REFER,
});
EXPECT_EQ(-1, link(file1_s1d1, file1_s1d3));
EXPECT_EQ(EACCES, errno);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer",
dir_s1d1));
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
"fs\\.make_reg,fs\\.refer", dir_s1d3));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(0, records.domain);
}
TEST_F(audit_layout1, refer_rename)
{
struct audit_records records;
EXPECT_EQ(0, unlink(file1_s1d3));
drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
.handled_access_fs = access_fs_16,
});
EXPECT_EQ(EACCES, test_rename(file1_s1d2, file1_s2d3));
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
"fs\\.remove_file,fs\\.refer", dir_s1d2));
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
"fs\\.remove_file,fs\\.make_reg,fs\\.refer",
dir_s2d3));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(0, records.domain);
}
TEST_F(audit_layout1, refer_exchange)
{
struct audit_records records;
EXPECT_EQ(0, unlink(file1_s1d3));
drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
.handled_access_fs = access_fs_16,
});
/*
* The only difference with the previous audit_layout1.refer_rename test is
* the extra ",fs\\.make_reg" blocked by the source directory.
*/
EXPECT_EQ(EACCES, test_exchange(file1_s1d2, file1_s2d3));
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
"fs\\.remove_file,fs\\.make_reg,fs\\.refer",
dir_s1d2));
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
"fs\\.remove_file,fs\\.make_reg,fs\\.refer",
dir_s2d3));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(0, records.domain);
}
/*
* This test checks that the audit record is correctly generated when the
* operation is only partially denied. This is the case for rename(2) when the
* source file is allowed to be referenced but the destination directory is not.
*
* This is also a regression test for commit d617f0d72d80 ("landlock: Optimize
* file path walks and prepare for audit support") and commit 058518c20920
* ("landlock: Align partial refer access checks with final ones").
*/
TEST_F(audit_layout1, refer_rename_half)
{
struct audit_records records;
const struct rule layer1[] = {
{
.path = dir_s2d2,
.access = LANDLOCK_ACCESS_FS_REFER,
},
{},
};
int ruleset_fd =
create_ruleset(_metadata, LANDLOCK_ACCESS_FS_REFER, layer1);
ASSERT_LE(0, ruleset_fd);
enforce_ruleset(_metadata, ruleset_fd);
ASSERT_EQ(0, close(ruleset_fd));
ASSERT_EQ(-1, rename(dir_s1d2, dir_s2d3));
ASSERT_EQ(EXDEV, errno);
/* Only half of the request is denied. */
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer",
dir_s1d1));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
}
TEST_F(audit_layout1, truncate)
{
struct audit_records records;
drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
.handled_access_fs = access_fs_16,
});
EXPECT_EQ(-1, truncate(file1_s1d3, 0));
EXPECT_EQ(EACCES, errno);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.truncate",
file1_s1d3));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
}
TEST_F(audit_layout1, ioctl_dev)
{
struct audit_records records;
int fd;
drop_access_rights(_metadata,
&(struct landlock_ruleset_attr){
.handled_access_fs =
access_fs_16 &
~LANDLOCK_ACCESS_FS_READ_FILE,
});
fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
ASSERT_LE(0, fd);
EXPECT_EQ(EACCES, ioctl_error(_metadata, fd, FIONREAD));
EXPECT_EQ(0, matches_log_fs_extra(_metadata, self->audit_fd,
"fs\\.ioctl_dev", "/dev/null",
" ioctlcmd=0x541b"));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
}
TEST_F(audit_layout1, mount)
{
struct audit_records records;
drop_access_rights(_metadata,
&(struct landlock_ruleset_attr){
.handled_access_fs =
LANDLOCK_ACCESS_FS_EXECUTE,
});
set_cap(_metadata, CAP_SYS_ADMIN);
EXPECT_EQ(-1, mount(NULL, dir_s3d2, NULL, MS_RDONLY, NULL));
EXPECT_EQ(EPERM, errno);
clear_cap(_metadata, CAP_SYS_ADMIN);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
"fs\\.change_topology", dir_s3d2));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
}
TEST_HARNESS_MAIN

View File

@ -20,6 +20,7 @@
#include <sys/syscall.h>
#include <sys/un.h>
#include "audit.h"
#include "common.h"
const short sock_port_start = (1 << 10);
@ -1868,4 +1869,135 @@ TEST_F(port_specific, bind_connect_1023)
EXPECT_EQ(0, close(bind_fd));
}
static int matches_log_tcp(const int audit_fd, const char *const blockers,
const char *const dir_addr, const char *const addr,
const char *const dir_port)
{
static const char log_template[] = REGEX_LANDLOCK_PREFIX
" blockers=%s %s=%s %s=1024$";
/*
* Max strlen(blockers): 16
* Max strlen(dir_addr): 5
* Max strlen(addr): 12
* Max strlen(dir_port): 4
*/
char log_match[sizeof(log_template) + 37];
int log_match_len;
log_match_len = snprintf(log_match, sizeof(log_match), log_template,
blockers, dir_addr, addr, dir_port);
if (log_match_len > sizeof(log_match))
return -E2BIG;
return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match,
NULL);
}
FIXTURE(audit)
{
struct service_fixture srv0;
struct audit_filter audit_filter;
int audit_fd;
};
FIXTURE_VARIANT(audit)
{
const char *const addr;
const struct protocol_variant prot;
};
/* clang-format off */
FIXTURE_VARIANT_ADD(audit, ipv4) {
/* clang-format on */
.addr = "127\\.0\\.0\\.1",
.prot = {
.domain = AF_INET,
.type = SOCK_STREAM,
},
};
/* clang-format off */
FIXTURE_VARIANT_ADD(audit, ipv6) {
/* clang-format on */
.addr = "::1",
.prot = {
.domain = AF_INET6,
.type = SOCK_STREAM,
},
};
FIXTURE_SETUP(audit)
{
ASSERT_EQ(0, set_service(&self->srv0, variant->prot, 0));
setup_loopback(_metadata);
set_cap(_metadata, CAP_AUDIT_CONTROL);
self->audit_fd = audit_init_with_exe_filter(&self->audit_filter);
EXPECT_LE(0, self->audit_fd);
disable_caps(_metadata);
};
FIXTURE_TEARDOWN(audit)
{
set_cap(_metadata, CAP_AUDIT_CONTROL);
EXPECT_EQ(0, audit_cleanup(self->audit_fd, &self->audit_filter));
clear_cap(_metadata, CAP_AUDIT_CONTROL);
}
TEST_F(audit, bind)
{
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
LANDLOCK_ACCESS_NET_CONNECT_TCP,
};
struct audit_records records;
int ruleset_fd, sock_fd;
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
sock_fd = socket_variant(&self->srv0);
ASSERT_LE(0, sock_fd);
EXPECT_EQ(-EACCES, bind_variant(sock_fd, &self->srv0));
EXPECT_EQ(0, matches_log_tcp(self->audit_fd, "net\\.bind_tcp", "saddr",
variant->addr, "src"));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
EXPECT_EQ(0, close(sock_fd));
}
TEST_F(audit, connect)
{
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
LANDLOCK_ACCESS_NET_CONNECT_TCP,
};
struct audit_records records;
int ruleset_fd, sock_fd;
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
sock_fd = socket_variant(&self->srv0);
ASSERT_LE(0, sock_fd);
EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv0));
EXPECT_EQ(0, matches_log_tcp(self->audit_fd, "net\\.connect_tcp",
"daddr", variant->addr, "dest"));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
EXPECT_EQ(0, close(sock_fd));
}
TEST_HARNESS_MAIN

View File

@ -4,6 +4,7 @@
*
* Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2019-2020 ANSSI
* Copyright © 2024-2025 Microsoft Corporation
*/
#define _GNU_SOURCE
@ -17,6 +18,7 @@
#include <sys/wait.h>
#include <unistd.h>
#include "audit.h"
#include "common.h"
/* Copied from security/yama/yama_lsm.c */
@ -434,4 +436,142 @@ TEST_F(hierarchy, trace)
_metadata->exit_code = KSFT_FAIL;
}
static int matches_log_ptrace(struct __test_metadata *const _metadata,
int audit_fd, const pid_t opid)
{
static const char log_template[] = REGEX_LANDLOCK_PREFIX
" blockers=ptrace opid=%d ocomm=\"ptrace_test\"$";
char log_match[sizeof(log_template) + 10];
int log_match_len;
log_match_len =
snprintf(log_match, sizeof(log_match), log_template, opid);
if (log_match_len > sizeof(log_match))
return -E2BIG;
return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match,
NULL);
}
FIXTURE(audit)
{
struct audit_filter audit_filter;
int audit_fd;
};
FIXTURE_SETUP(audit)
{
disable_caps(_metadata);
set_cap(_metadata, CAP_AUDIT_CONTROL);
self->audit_fd = audit_init_with_exe_filter(&self->audit_filter);
EXPECT_LE(0, self->audit_fd);
clear_cap(_metadata, CAP_AUDIT_CONTROL);
}
FIXTURE_TEARDOWN_PARENT(audit)
{
EXPECT_EQ(0, audit_cleanup(-1, NULL));
}
/* Test PTRACE_TRACEME and PTRACE_ATTACH for parent and child. */
TEST_F(audit, trace)
{
pid_t child;
int status;
int pipe_child[2], pipe_parent[2];
int yama_ptrace_scope;
char buf_parent;
struct audit_records records;
/* Makes sure there is no superfluous logged records. */
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(0, records.domain);
yama_ptrace_scope = get_yama_ptrace_scope();
ASSERT_LE(0, yama_ptrace_scope);
if (yama_ptrace_scope > YAMA_SCOPE_DISABLED)
TH_LOG("Incomplete tests due to Yama restrictions (scope %d)",
yama_ptrace_scope);
/*
* Removes all effective and permitted capabilities to not interfere
* with cap_ptrace_access_check() in case of PTRACE_MODE_FSCREDS.
*/
drop_caps(_metadata);
ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
char buf_child;
ASSERT_EQ(0, close(pipe_parent[1]));
ASSERT_EQ(0, close(pipe_child[0]));
/* Waits for the parent to be in a domain, if any. */
ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
/* Tests child PTRACE_TRACEME. */
EXPECT_EQ(-1, ptrace(PTRACE_TRACEME));
EXPECT_EQ(EPERM, errno);
/* We should see the child process. */
EXPECT_EQ(0, matches_log_ptrace(_metadata, self->audit_fd,
getpid()));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
/* Checks for a domain creation. */
EXPECT_EQ(1, records.domain);
/*
* Signals that the PTRACE_ATTACH test is done and the
* PTRACE_TRACEME test is ongoing.
*/
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
/* Waits for the parent PTRACE_ATTACH test. */
ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
_exit(_metadata->exit_code);
return;
}
ASSERT_EQ(0, close(pipe_child[1]));
ASSERT_EQ(0, close(pipe_parent[0]));
create_domain(_metadata);
/* Signals that the parent is in a domain. */
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
/*
* Waits for the child to test PTRACE_ATTACH on the parent and start
* testing PTRACE_TRACEME.
*/
ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1));
/* The child should not be traced by the parent. */
EXPECT_EQ(-1, ptrace(PTRACE_DETACH, child, NULL, 0));
EXPECT_EQ(ESRCH, errno);
/* Tests PTRACE_ATTACH on the child. */
EXPECT_EQ(-1, ptrace(PTRACE_ATTACH, child, NULL, 0));
EXPECT_EQ(EPERM, errno);
EXPECT_EQ(0, matches_log_ptrace(_metadata, self->audit_fd, child));
/* Signals that the parent PTRACE_ATTACH test is done. */
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
ASSERT_EQ(child, waitpid(child, &status, 0));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
/* Makes sure there is no superfluous logged records. */
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(0, records.domain);
}
TEST_HARNESS_MAIN

View File

@ -20,6 +20,7 @@
#include <sys/wait.h>
#include <unistd.h>
#include "audit.h"
#include "common.h"
#include "scoped_common.h"
@ -267,6 +268,116 @@ TEST_F(scoped_domains, connect_to_child)
_metadata->exit_code = KSFT_FAIL;
}
FIXTURE(scoped_audit)
{
struct service_fixture dgram_address;
struct audit_filter audit_filter;
int audit_fd;
};
FIXTURE_SETUP(scoped_audit)
{
disable_caps(_metadata);
memset(&self->dgram_address, 0, sizeof(self->dgram_address));
set_unix_address(&self->dgram_address, 1);
set_cap(_metadata, CAP_AUDIT_CONTROL);
self->audit_fd = audit_init_with_exe_filter(&self->audit_filter);
EXPECT_LE(0, self->audit_fd);
drop_caps(_metadata);
}
FIXTURE_TEARDOWN_PARENT(scoped_audit)
{
EXPECT_EQ(0, audit_cleanup(-1, NULL));
}
/* python -c 'print(b"\0selftests-landlock-abstract-unix-".hex().upper())' */
#define ABSTRACT_SOCKET_PATH_PREFIX \
"0073656C6674657374732D6C616E646C6F636B2D61627374726163742D756E69782D"
/*
* Simpler version of scoped_domains.connect_to_child, but with audit tests.
*/
TEST_F(scoped_audit, connect_to_child)
{
pid_t child;
int err_dgram, status;
int pipe_child[2], pipe_parent[2];
char buf;
int dgram_client;
struct audit_records records;
/* Makes sure there is no superfluous logged records. */
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(0, records.domain);
ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
int dgram_server;
EXPECT_EQ(0, close(pipe_parent[1]));
EXPECT_EQ(0, close(pipe_child[0]));
/* Waits for the parent to be in a domain. */
ASSERT_EQ(1, read(pipe_parent[0], &buf, 1));
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));
/* Signals to the parent that child is listening. */
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
/* Waits to connect. */
ASSERT_EQ(1, read(pipe_parent[0], &buf, 1));
EXPECT_EQ(0, close(dgram_server));
_exit(_metadata->exit_code);
return;
}
EXPECT_EQ(0, close(pipe_child[1]));
EXPECT_EQ(0, close(pipe_parent[0]));
create_scoped_domain(_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
/* Signals that the parent is in a domain, if any. */
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
dgram_client = socket(AF_UNIX, SOCK_DGRAM, 0);
ASSERT_LE(0, dgram_client);
/* 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);
EXPECT_EQ(-1, err_dgram);
EXPECT_EQ(EPERM, errno);
EXPECT_EQ(
0,
audit_match_record(
self->audit_fd, AUDIT_LANDLOCK_ACCESS,
REGEX_LANDLOCK_PREFIX
" blockers=scope\\.abstract_unix_socket path=" ABSTRACT_SOCKET_PATH_PREFIX
"[0-9A-F]\\+$",
NULL));
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
EXPECT_EQ(0, close(dgram_client));
ASSERT_EQ(child, waitpid(child, &status, 0));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
}
FIXTURE(scoped_vs_unscoped)
{
struct service_fixture parent_stream_address, parent_dgram_address,

View File

@ -249,47 +249,67 @@ TEST_F(scoped_domains, check_access_signal)
_metadata->exit_code = KSFT_FAIL;
}
static int thread_pipe[2];
enum thread_return {
THREAD_INVALID = 0,
THREAD_SUCCESS = 1,
THREAD_ERROR = 2,
THREAD_TEST_FAILED = 3,
};
void *thread_func(void *arg)
static void *thread_sync(void *arg)
{
const int pipe_read = *(int *)arg;
char buf;
if (read(thread_pipe[0], &buf, 1) != 1)
if (read(pipe_read, &buf, 1) != 1)
return (void *)THREAD_ERROR;
return (void *)THREAD_SUCCESS;
}
TEST(signal_scoping_threads)
TEST(signal_scoping_thread_before)
{
pthread_t no_sandbox_thread, scoped_thread;
pthread_t no_sandbox_thread;
enum thread_return ret = THREAD_INVALID;
int thread_pipe[2];
drop_caps(_metadata);
ASSERT_EQ(0, pipe2(thread_pipe, O_CLOEXEC));
ASSERT_EQ(0,
pthread_create(&no_sandbox_thread, NULL, thread_func, NULL));
ASSERT_EQ(0, pthread_create(&no_sandbox_thread, NULL, thread_sync,
&thread_pipe[0]));
/* Restricts the domain after creating the first thread. */
/* Enforces restriction after creating the thread. */
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
ASSERT_EQ(EPERM, pthread_kill(no_sandbox_thread, 0));
ASSERT_EQ(1, write(thread_pipe[1], ".", 1));
ASSERT_EQ(0, pthread_create(&scoped_thread, NULL, thread_func, NULL));
ASSERT_EQ(0, pthread_kill(scoped_thread, 0));
ASSERT_EQ(1, write(thread_pipe[1], ".", 1));
EXPECT_EQ(0, pthread_kill(no_sandbox_thread, 0));
EXPECT_EQ(1, write(thread_pipe[1], ".", 1));
EXPECT_EQ(0, pthread_join(no_sandbox_thread, (void **)&ret));
EXPECT_EQ(THREAD_SUCCESS, ret);
EXPECT_EQ(0, close(thread_pipe[0]));
EXPECT_EQ(0, close(thread_pipe[1]));
}
TEST(signal_scoping_thread_after)
{
pthread_t scoped_thread;
enum thread_return ret = THREAD_INVALID;
int thread_pipe[2];
drop_caps(_metadata);
ASSERT_EQ(0, pipe2(thread_pipe, O_CLOEXEC));
/* Enforces restriction before creating the thread. */
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
ASSERT_EQ(0, pthread_create(&scoped_thread, NULL, thread_sync,
&thread_pipe[0]));
EXPECT_EQ(0, pthread_kill(scoped_thread, 0));
EXPECT_EQ(1, write(thread_pipe[1], ".", 1));
EXPECT_EQ(0, pthread_join(scoped_thread, (void **)&ret));
EXPECT_EQ(THREAD_SUCCESS, ret);
@ -297,6 +317,64 @@ TEST(signal_scoping_threads)
EXPECT_EQ(0, close(thread_pipe[1]));
}
struct thread_setuid_args {
int pipe_read, new_uid;
};
void *thread_setuid(void *ptr)
{
const struct thread_setuid_args *arg = ptr;
char buf;
if (read(arg->pipe_read, &buf, 1) != 1)
return (void *)THREAD_ERROR;
/* libc's setuid() should update all thread's credentials. */
if (getuid() != arg->new_uid)
return (void *)THREAD_TEST_FAILED;
return (void *)THREAD_SUCCESS;
}
TEST(signal_scoping_thread_setuid)
{
struct thread_setuid_args arg;
pthread_t no_sandbox_thread;
enum thread_return ret = THREAD_INVALID;
int pipe_parent[2];
int prev_uid;
disable_caps(_metadata);
/* This test does not need to be run as root. */
prev_uid = getuid();
arg.new_uid = prev_uid + 1;
EXPECT_LT(0, arg.new_uid);
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
arg.pipe_read = pipe_parent[0];
/* Capabilities must be set before creating a new thread. */
set_cap(_metadata, CAP_SETUID);
ASSERT_EQ(0, pthread_create(&no_sandbox_thread, NULL, thread_setuid,
&arg));
/* Enforces restriction after creating the thread. */
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
EXPECT_NE(arg.new_uid, getuid());
EXPECT_EQ(0, setuid(arg.new_uid));
EXPECT_EQ(arg.new_uid, getuid());
EXPECT_EQ(1, write(pipe_parent[1], ".", 1));
EXPECT_EQ(0, pthread_join(no_sandbox_thread, (void **)&ret));
EXPECT_EQ(THREAD_SUCCESS, ret);
clear_cap(_metadata, CAP_SETUID);
EXPECT_EQ(0, close(pipe_parent[0]));
EXPECT_EQ(0, close(pipe_parent[1]));
}
const short backlog = 10;
static volatile sig_atomic_t signal_received;

View File

@ -0,0 +1,131 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Write in a pipe, wait, sandbox itself, test sandboxing, and wait again.
*
* Used by audit_exec.flags from audit_test.c
*
* Copyright © 2024-2025 Microsoft Corporation
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <linux/landlock.h>
#include <linux/prctl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <unistd.h>
#include "wrappers.h"
static int sync_with(int pipe_child, int pipe_parent)
{
char buf;
/* Signals that we are waiting. */
if (write(pipe_child, ".", 1) != 1) {
perror("Failed to write to first argument");
return 1;
}
/* Waits for the parent do its test. */
if (read(pipe_parent, &buf, 1) != 1) {
perror("Failed to write to the second argument");
return 1;
}
return 0;
}
int main(int argc, char *argv[])
{
const struct landlock_ruleset_attr layer2 = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
};
const struct landlock_ruleset_attr layer3 = {
.scoped = LANDLOCK_SCOPE_SIGNAL,
};
int err, pipe_child, pipe_parent, ruleset_fd;
/* The first argument must be the file descriptor number of a pipe. */
if (argc != 3) {
fprintf(stderr, "Wrong number of arguments (not two)\n");
return 1;
}
pipe_child = atoi(argv[1]);
pipe_parent = atoi(argv[2]);
/* PR_SET_NO_NEW_PRIVS already set by parent. */
/* First step to test parent's layer1. */
err = sync_with(pipe_child, pipe_parent);
if (err)
return err;
/* Tries to send a signal, denied by layer1. */
if (!kill(getppid(), 0)) {
fprintf(stderr, "Successfully sent a signal to the parent");
return 1;
}
/* Second step to test parent's layer1 and our layer2. */
err = sync_with(pipe_child, pipe_parent);
if (err)
return err;
ruleset_fd = landlock_create_ruleset(&layer2, sizeof(layer2), 0);
if (ruleset_fd < 0) {
perror("Failed to create the layer2 ruleset");
return 1;
}
if (landlock_restrict_self(ruleset_fd, 0)) {
perror("Failed to restrict self");
return 1;
}
close(ruleset_fd);
/* Tries to send a signal, denied by layer1. */
if (!kill(getppid(), 0)) {
fprintf(stderr, "Successfully sent a signal to the parent");
return 1;
}
/* Tries to open ., denied by layer2. */
if (open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC) >= 0) {
fprintf(stderr, "Successfully opened /");
return 1;
}
/* Third step to test our layer2 and layer3. */
err = sync_with(pipe_child, pipe_parent);
if (err)
return err;
ruleset_fd = landlock_create_ruleset(&layer3, sizeof(layer3), 0);
if (ruleset_fd < 0) {
perror("Failed to create the layer3 ruleset");
return 1;
}
if (landlock_restrict_self(ruleset_fd, 0)) {
perror("Failed to restrict self");
return 1;
}
close(ruleset_fd);
/* Tries to open ., denied by layer2. */
if (open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC) >= 0) {
fprintf(stderr, "Successfully opened /");
return 1;
}
/* Tries to send a signal, denied by layer3. */
if (!kill(getppid(), 0)) {
fprintf(stderr, "Successfully sent a signal to the parent");
return 1;
}
return 0;
}