Hi,
This patch series adds audit support to Landlock.
Logging denied requests is useful for different use cases:
- sysadmins: to look for users' issues,
- security experts: to detect attack attempts,
- power users: to understand denials,
- developers: to ease sandboxing support and get feedback from users.
Because of its unprivileged nature, Landlock can compose standalone
security policies (i.e. domains). To make logs useful, they need to
contain the most relevant Landlock domain that denied an action, and the
reason of such denial. This translates to the latest nested domain and
the related blockers: missing access rights or other kind of
restrictions.
# Changes from previous version
Remove the AUDIT_EXE_LANDLOCK_DENY audit rule and add 2 new
landlock_restrict_self(2) flags to filter Landlock audit events, which
makes 3 flags:
- LANDLOCK_RESTRICT_SELF_QUIET: do not log any denied access because of
this new domain.
- LANDLOCK_RESTRICT_SELF_QUIET_DESCENDENTS: do not log denied access
from child domains.
- LANDLOCK_RESTRICT_SELF_LOG_CROSS_EXEC: log denied access for processes
resulting from an execve(2), which is not the case by default anymore.
One patch was merged in mainline: 7ccbe076d987 ("lsm: Only build
lsm_audit.c if CONFIG_SECURITY and CONFIG_AUDIT are set").
# Design
Log records are created for any denied actions caused by a Landlock
policy, which means that a well-sandboxed applications should not log
anything except for unattended access requests that might be the result
of attacks or bugs.
However, sandbox tools creating restricted environments could lead to
abundant log entries because the sandboxed processes may not be aware of
the related restrictions. To avoid log spam, the
landlock_restrict_self(2) syscall gets a new
LANDLOCK_RESTRICT_SELF_QUIET flag to not log denials related to this
specific domain. Except for well-understood exceptions, this flag
should not be set. Indeed, applications sandboxing themselves should
only try to bypass their own sandbox if they are compromised, which
should ring a bell thanks to log events.
When an action is denied, the related Landlock domain ID is specified.
If this domain was not previously described in a log record, one is
created. This record contains the domain ID, its creation time, and
informations about the process that enforced the restriction (at the
time of the call to landlock_restrict_self): PID, UID, executable path,
and name (comm).
This new approach also brings building blocks for an upcoming
unprivileged introspection interface. The unique Landlock IDs will be
useful to tie audit log entries to running processes, and to get
properties of the related Landlock domains. This will replace the
previously logged ruleset properties.
# Samples
Here are two examples of log events (see serial numbers):
$ LL_FS_RO=/ LL_FS_RW=/ LL_SCOPED=s LL_FORCE_LOG=1 ./sandboxer kill 1
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
$ LL_FS_RO=/ LL_FS_RW=/tmp LL_FORCE_LOG=1 ./sandboxer sh -c "echo > /etc/passwd"
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
# Future changes
I'll add more tests to check each kind of denied access.
# Previous versions
v4: https://lore.kernel.org/r/20250108154338.1129069-1-mic@digikod.net
v3: https://lore.kernel.org/r/20241122143353.59367-1-mic@digikod.net
v2: https://lore.kernel.org/r/20241022161009.982584-1-mic@digikod.net
v1: https://lore.kernel.org/r/20230921061641.273654-1-mic@digikod.net
Regards,
Mickaël Salaün (24):
lsm: Add audit_log_lsm_data() helper
landlock: Add unique ID generator
landlock: Move domain hierarchy management
landlock: Prepare to use credential instead of domain for filesystem
landlock: Prepare to use credential instead of domain for network
landlock: Prepare to use credential instead of domain for scope
landlock: Prepare to use credential instead of domain for fowner
landlock: Identify domain execution crossing
landlock: Add AUDIT_LANDLOCK_ACCESS and log ptrace denials
landlock: Add AUDIT_LANDLOCK_DOMAIN and log domain status
landlock: Log mount-related denials
landlock: Log file-related denials
landlock: Log truncate and IOCTL denials
landlock: Log TCP bind and connect denials
landlock: Log scoped denials
landlock: Add LANDLOCK_RESTRICT_SELF_QUIET
landlock: Add LANDLOCK_RESTRICT_SELF_QUIET_SUBDOMAINS
landlock: Add LANDLOCK_RESTRICT_SELF_LOG_CROSS_EXEC
samples/landlock: Enable users to log sandbox denials
selftests/landlock: Extend tests for landlock_restrict_self()'s flags
selftests/landlock: Add tests for audit and
LANDLOCK_RESTRICT_SELF_QUIET
selftests/landlock: Test audit with restrict flags
selftests/landlock: Add audit tests for ptrace
landlock: Add audit documentation
Documentation/admin-guide/LSM/index.rst | 1 +
Documentation/admin-guide/LSM/landlock.rst | 157 ++++++
Documentation/security/landlock.rst | 7 +
Documentation/userspace-api/landlock.rst | 9 +-
MAINTAINERS | 1 +
include/linux/lsm_audit.h | 8 +
include/uapi/linux/audit.h | 4 +-
include/uapi/linux/landlock.h | 31 ++
samples/landlock/sandboxer.c | 37 +-
security/landlock/.kunitconfig | 2 +
security/landlock/Makefile | 5 +
security/landlock/access.h | 23 +
security/landlock/audit.c | 513 ++++++++++++++++++
security/landlock/audit.h | 77 +++
security/landlock/cred.c | 26 +-
security/landlock/cred.h | 65 +++
security/landlock/domain.c | 264 +++++++++
security/landlock/domain.h | 158 ++++++
security/landlock/fs.c | 279 ++++++++--
security/landlock/fs.h | 21 +-
security/landlock/id.c | 249 +++++++++
security/landlock/id.h | 25 +
security/landlock/limits.h | 4 +
security/landlock/net.c | 74 ++-
security/landlock/ruleset.c | 33 +-
security/landlock/ruleset.h | 47 +-
security/landlock/setup.c | 2 +
security/landlock/syscalls.c | 50 +-
security/landlock/task.c | 232 ++++++--
security/lsm_audit.c | 27 +-
tools/testing/kunit/configs/all_tests.config | 2 +
tools/testing/selftests/landlock/Makefile | 6 +-
tools/testing/selftests/landlock/audit.h | 358 ++++++++++++
tools/testing/selftests/landlock/audit_test.c | 425 +++++++++++++++
tools/testing/selftests/landlock/base_test.c | 43 +-
tools/testing/selftests/landlock/common.h | 3 +
tools/testing/selftests/landlock/config | 1 +
.../testing/selftests/landlock/ptrace_test.c | 67 ++-
.../selftests/landlock/wait-pipe-sandbox.c | 131 +++++
39 files changed, 3244 insertions(+), 223 deletions(-)
create mode 100644 Documentation/admin-guide/LSM/landlock.rst
create mode 100644 security/landlock/audit.c
create mode 100644 security/landlock/audit.h
create mode 100644 security/landlock/domain.c
create mode 100644 security/landlock/domain.h
create mode 100644 security/landlock/id.c
create mode 100644 security/landlock/id.h
create mode 100644 tools/testing/selftests/landlock/audit.h
create mode 100644 tools/testing/selftests/landlock/audit_test.c
create mode 100644 tools/testing/selftests/landlock/wait-pipe-sandbox.c
base-commit: 69e858e0b8b2ea07759e995aa383e8780d9d140c
--
2.48.1
Extract code from dump_common_audit_data() into the audit_log_lsm_data()
helper. This helps reuse common LSM audit data while not abusing
AUDIT_AVC records because of the common_lsm_audit() helper.
Cc: Casey Schaufler <casey@schaufler-ca.com>
Cc: James Morris <jmorris@namei.org>
Cc: Serge E. Hallyn <serge@hallyn.com>
Acked-by: Paul Moore <paul@paul-moore.com>
Depends-on: 7ccbe076d987 ("lsm: Only build lsm_audit.c if CONFIG_SECURITY and CONFIG_AUDIT are set")
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-2-mic@digikod.net
---
Changes since v4:
- Add Depends-on tag.
Changes since v3:
- Rebase on top of the v6.13's get_task_comm() fix.
- Add Acked-by Paul.
Changes since v1:
- Fix commit message (spotted by Paul).
- Constify dump_common_audit_data()'s and audit_log_lsm_data()'s "a"
argument.
- Fix build without CONFIG_NET: see previous patch.
---
include/linux/lsm_audit.h | 8 ++++++++
security/lsm_audit.c | 27 ++++++++++++++++++---------
2 files changed, 26 insertions(+), 9 deletions(-)
Landlock IDs can be generated to uniquely identify Landlock objects.
For now, only Landlock domains get an ID at creation time. These IDs
map to immutable domain hierarchies.
Landlock IDs have important properties:
- They are unique during the lifetime of the running system thanks to
the 64-bit values: at worse, 2^60 - 2*2^32 useful IDs.
- They are always greater than 2^32 and must then be stored in 64-bit
integer types.
- The initial ID (at boot time) is randomly picked between 2^32 and
2^33, which limits collisions in logs between different boots.
- IDs are sequential, which enables users to order them.
- IDs may not be consecutive but increase with a random 2^4 step, which
limits side channels.
Such IDs can be exposed to unprivileged processes, even if it is not the
case with this audit patch series. The domain IDs will be useful for
user space to identify sandboxes and get their properties.
These Landlock IDs are more robust that other absolute kernel IDs such
as pipe's inodes which rely on a shared global counter.
For checkpoint/restore features (i.e. CRIU), we could easily implement a
privileged interface (e.g. sysfs) to set the next ID counter.
IDR/IDA are not used because we only need a bijection from Landlock
objects to Landlock IDs, and we must not recycle IDs. This enables us
to identify all Landlock objects during the lifetime of the system (e.g.
in logs), but not to access an object from an ID nor know if an ID is
assigned. Using a counter is simpler, it scales (i.e. avoids growing
memory footprint), and it does not require locking. We'll use proper
file descriptors (with IDs used as inode numbers) to access Landlock
objects.
Cc: Günther Noack <gnoack@google.com>
Cc: Paul Moore <paul@paul-moore.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-3-mic@digikod.net
---
Changes since v3:
- Rename landlock_get_id_range() helper to reflect the "range" of IDs.
- Add docstring for landlock_get_id_range().
Changes since v2:
- Extend commit message.
- Rename global_counter to next_id.
- Fix KUnit's test __init types, spotted by kernel test robot.
Changes since v1:
- New patch.
---
security/landlock/.kunitconfig | 2 +
security/landlock/Makefile | 2 +
security/landlock/id.c | 249 +++++++++++++++++++
security/landlock/id.h | 25 ++
security/landlock/setup.c | 2 +
tools/testing/kunit/configs/all_tests.config | 2 +
6 files changed, 282 insertions(+)
create mode 100644 security/landlock/id.c
create mode 100644 security/landlock/id.h
Create a new domain.h file containing the struct landlock_hierarchy
definition and helpers. This type will grow with audit support. This
also prepares for a new domain type.
Cc: Günther Noack <gnoack@google.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-4-mic@digikod.net
---
Changes since v4:
- Revert v3 changes because of the new audit rule patch removal.
Changes since v3:
- Export landlock_get_hierarchy() and landlock_put_hierarchy().
- Clean up Makefile entries.
Changes since v1:
- New patch.
---
security/landlock/domain.h | 48 +++++++++++++++++++++++++++++++++++++
security/landlock/ruleset.c | 21 +++-------------
security/landlock/ruleset.h | 17 +------------
security/landlock/task.c | 1 +
4 files changed, 53 insertions(+), 34 deletions(-)
create mode 100644 security/landlock/domain.h
This cosmetic change that is needed for audit support, specifically to
be able to filter according to cross-execution boundaries.
Add landlock_get_applicable_subject(), mainly a copy of
landlock_get_applicable_domain(), and which will fully replace it in a
following commit.
Optimize current_check_access_path() to only handle the access request.
Partially replace get_current_fs_domain() with explicit calls to
landlock_get_applicable_subject(). The remaining ones will follow with
more changes.
Remove explicit domain->num_layers check which is now part of the
landlock_get_applicable_subject() call.
Cc: Günther Noack <gnoack@google.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-5-mic@digikod.net
---
Changes since v4:
- New patch.
---
security/landlock/cred.h | 50 +++++++++++++++++++++++++++++++
security/landlock/fs.c | 65 +++++++++++++++++++++++-----------------
2 files changed, 88 insertions(+), 27 deletions(-)
This cosmetic change that is needed for audit support, specifically to
be able to filter according to cross-execution boundaries.
struct landlock_file_security's size stay the same for now but it will
increase with struct landlock_cred_security's size.
Only save Landlock domain in hook_file_set_fowner() if the current
domain has LANDLOCK_SCOPE_SIGNAL, which was previously done for each
hook_file_send_sigiotask() calls. This should improve a bit
performances.
Replace hardcoded LANDLOCK_SCOPE_SIGNAL with the signal_scope.scope
variable.
Use scoped guards for RCU read-side critical sections.
Cc: Günther Noack <gnoack@google.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-8-mic@digikod.net
---
Changes since v4:
- New patch.
---
security/landlock/fs.c | 26 ++++++++++++++++++++------
security/landlock/fs.h | 12 +++++++-----
security/landlock/task.c | 25 ++++++++++++++++---------
3 files changed, 43 insertions(+), 20 deletions(-)
@@ -1641,17 +1641,31 @@ static int hook_file_ioctl_compat(struct file *file, unsigned int cmd,staticvoidhook_file_set_fowner(structfile*file){-structlandlock_ruleset*new_dom,*prev_dom;+staticconststructaccess_maskssignal_scope={+.scope=LANDLOCK_SCOPE_SIGNAL,+};+conststructlandlock_cred_security*new_subject;+structlandlock_cred_security*fown_subject;+structlandlock_ruleset*prev_dom;/**Lockalreadyheldby__f_setown(),seecommit26f204380a3c("fs: Fix*file_set_fownerLSMhookinconsistencies").*/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;++fown_subject=&landlock_file(file)->fown_subject;+prev_dom=fown_subject->domain;+new_subject=landlock_get_applicable_subject(current_cred(),+signal_scope,NULL);+if(new_subject){+*fown_subject=*new_subject;+}else{+staticconststructlandlock_cred_securityempty={};++*fown_subject=empty;+}+landlock_get_ruleset(fown_subject->domain);/* Called in an RCU read-side critical section. */landlock_put_ruleset_deferred(prev_dom);
@@ -288,22 +288,29 @@ static int hook_task_kill(struct task_struct *const p,staticinthook_file_send_sigiotask(structtask_struct*tsk,structfown_struct*fown,intsignum){-conststructlandlock_ruleset*dom;+conststructlandlock_cred_security*subject;boolis_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)+/*+*Quickreturnforunownedsocket.+*+*subject->domainhasalreadybeenfilteredwhensavedby+*hook_file_set_fowner(),sothereisnoneedtocall+*landlock_get_applicable_subject()here.+*/+if(!subject->domain)return0;-rcu_read_lock();-is_scoped=domain_is_scoped(dom,landlock_get_task_domain(tsk),-LANDLOCK_SCOPE_SIGNAL);-rcu_read_unlock();+scoped_guard(rcu)+{+is_scoped=domain_is_scoped(subject->domain,+landlock_get_task_domain(tsk),+signal_scope.scope);+}if(is_scoped)return-EPERM;
This cosmetic change that is needed for audit support, specifically to
be able to filter according to cross-execution boundaries.
Optimize current_check_access_socket() to only handle the access
request.
Remove explicit domain->num_layers check which is now part of the
landlock_get_applicable_subject() call.
Cc: Günther Noack <gnoack@google.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-6-mic@digikod.net
---
Changes since v4:
- New patch.
---
security/landlock/net.c | 23 ++++++++++-------------
1 file changed, 10 insertions(+), 13 deletions(-)
Extend struct landlock_cred_security with a domain_exec bitmask
identifying if the current task created its domain. This is reset on
cross-execution.
Cc: Günther Noack <gnoack@google.com>
Cc: Paul Moore <paul@paul-moore.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-9-mic@digikod.net
---
Changes since v4:
- New patch.
---
security/landlock/cred.c | 26 ++++++++++++++++++++++----
security/landlock/cred.h | 15 +++++++++++++++
security/landlock/syscalls.c | 5 +++++
3 files changed, 42 insertions(+), 4 deletions(-)
Add a new AUDIT_LANDLOCK_ACCESS record type dedicated to an access
request denied by a Landlock domain. AUDIT_LANDLOCK_ACCESS indicates
that something unexpected happened.
For now, only denied access are logged, which means that any
AUDIT_LANDLOCK_ACCESS record is always followed by a SYSCALL record with
"success=no". However, log parsers should check this syscall property
because this is the only sign that a request was denied. Indeed, we
could have "success=yes" if Landlock would support a "permissive" mode.
We could also add a new field for this mode to AUDIT_LANDLOCK_DOMAIN
(see following commit).
By default, the only logged access requests are those coming from the
same executed program that enforced the Landlock restriction on itself.
In other words, no audit record are created for a task after it called
execve(2). This is required to avoid log spam because programs may only
be aware of their own restrictions, but not the inherited ones.
Following commits will allow to conditionally generate
AUDIT_LANDLOCK_ACCESS records according to dedicated
landlock_restrict_self(2)'s flags.
The AUDIT_LANDLOCK_ACCESS message contains:
- the "domain" ID restricting the action on an object,
- the "blockers" that are missing to allow the requested access,
- a set of fields identifying the related object (e.g. task identified
with "opid" and "ocomm").
The blockers are implicit restrictions (e.g. ptrace), or explicit access
rights (e.g. filesystem), or explicit scopes (e.g. signal). This field
contains a list of at least one element, each separated with a comma.
The initial blocker is "ptrace", which describe all implicit Landlock
restrictions related to ptrace (e.g. deny tracing of tasks outside a
sandbox).
Add audit support to ptrace_access_check and ptrace_traceme hooks. For
the ptrace_access_check case, we log the current/parent domain and the
child task. For the ptrace_traceme case, we log the parent domain and
the parent task. Indeed, the requester is the current task, but the
action would be performed by the parent task.
Audit event sample:
type=LANDLOCK_ACCESS msg=audit(1729738800.349:44): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd"
type=SYSCALL msg=audit(1729738800.349:44): arch=c000003e syscall=101 success=no [...] pid=300 auid=0
A following commit adds user documentation.
Add KUnit tests to check reading of domain ID relative to layer level.
The quick return for non-landlocked tasks is moved from task_ptrace() to
each LSM hooks.
Because the landlock_log_denial() function is only called when an access
is denied, the compiler should be able to optimize the struct
landlock_request initializations. It is not useful to inline the
audit_enabled check because other computation are performed anyway, and
by the same landlock_log_denia() code.
Use scoped guards for RCU read-side critical sections.
Cc: Günther Noack <gnoack@google.com>
Cc: Paul Moore <paul@paul-moore.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-10-mic@digikod.net
---
Changes since v4:
- Rename AUDIT_LANDLOCK_DENY to AUDIT_LANDLOCK_ACCESS, requested by
Paul.
- Make landlock_log_denial() get Landlock credential instead of Landlock
domain to be able to filter on the domain_exe variable.
- Rebase on top of the migration from struct landlock_ruleset to struct
landlock_cred_security.
- Rename landlock_init_current_hierarchy() to
landlock_init_hierarchy_log().
- Rebase on top of the scoped guard patches.
- By default, do not log denials after an execution.
- Use scoped guards for RCU read-side critical sections.
Changes since v3:
- Extend commit message.
Changes since v2:
- Log domain IDs as hexadecimal number: this is a more compact notation
(i.e. at least one less digit), it improves alignment in logs, and it
makes most IDs start with 1 as leading digit (because of the 2^32
minimal value). Do not use the "0x" prefix that would add useless
data to logs.
- Constify function arguments.
- Clean up Makefile entries.
Changes since v1:
- Move most audit code to this patch.
- Rebase on the TCP patch series.
- Don't log missing access right: simplify and make it generic for rule
types.
- Don't log errno and then don't wrap the error with
landlock_log_request(), as suggested by Jeff.
- Add a WARN_ON_ONCE() check to never dereference null pointers.
- Only log when audit is enabled.
- Don't log task's PID/TID with log_task() because it would be redundant
with the SYSCALL record.
- Move the "op" in front and rename "domain" to "denying_domain" to make
it more consistent with other entries.
- Don't update the request with the domain ID but add an helper to get
it from the layer masks (and in a following commit with a struct
file).
- Revamp get_domain_id_from_layer_masks() into
get_level_from_layer_masks().
- For ptrace_traceme, log the parent domain instead of the current one.
- Add documentation.
- Rename AUDIT_LANDLOCK_DENIAL to AUDIT_LANDLOCK_DENY.
- Only log the domain ID and the target task.
- Log "blockers", which are either implicit restrictions (e.g. ptrace)
or explicit access rights (e.g. filesystem), or scopes (e.g. signal).
- Don't log LSM hook names/operations.
- Pick an audit event ID folling the IPE ones.
- Add KUnit tests.
---
include/uapi/linux/audit.h | 3 +-
security/landlock/Makefile | 5 +-
security/landlock/audit.c | 146 ++++++++++++++++++++++++++++++++++++
security/landlock/audit.h | 53 +++++++++++++
security/landlock/domain.c | 28 +++++++
security/landlock/domain.h | 22 ++++++
security/landlock/ruleset.c | 6 ++
security/landlock/task.c | 96 ++++++++++++++++++------
8 files changed, 334 insertions(+), 25 deletions(-)
create mode 100644 security/landlock/audit.c
create mode 100644 security/landlock/audit.h
create mode 100644 security/landlock/domain.c
@@ -505,6 +506,7 @@ static void free_ruleset_work(struct work_struct *const work)free_ruleset(ruleset);}+/* Only called by hook_cred_free(). */voidlandlock_put_ruleset_deferred(structlandlock_ruleset*construleset){if(ruleset&&refcount_dec_and_test(&ruleset->usage)){
@@ -39,41 +41,29 @@ static bool domain_scope_le(const struct landlock_ruleset *const parent,{conststructlandlock_hierarchy*walker;+/* Quick return for non-landlocked tasks. */if(!parent)returntrue;+if(!child)returnfalse;+for(walker=child->hierarchy;walker;walker=walker->parent){if(walker==parent->hierarchy)/* @parent is in the scoped hierarchy of @child. */returntrue;}+/* There is no relationship between @parent and @child. */returnfalse;}-staticbooltask_is_scoped(conststructtask_struct*constparent,-conststructtask_struct*constchild)-{-boolis_scoped;-conststructlandlock_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();-returnis_scoped;-}--staticinttask_ptrace(conststructtask_struct*constparent,-conststructtask_struct*constchild)+staticintdomain_ptrace(conststructlandlock_ruleset*constparent,+conststructlandlock_ruleset*constchild){-/* Quick return for non-landlocked tasks. */-if(!landlocked(parent))-return0;-if(task_is_scoped(parent,child))+if(domain_scope_le(parent,child))return0;+return-EPERM;}
@@ -93,7 +83,38 @@ static int task_ptrace(const struct task_struct *const parent,staticinthook_ptrace_access_check(structtask_struct*constchild,constunsignedintmode){-returntask_ptrace(current,child);+conststructlandlock_cred_security*parent_subject;+conststructlandlock_ruleset*child_dom;+structlandlock_requestrequest={+.type=LANDLOCK_REQUEST_PTRACE,+.audit={+.type=LSM_AUDIT_DATA_TASK,+.u.tsk=child,+},+};+interr;++/* Quick return for non-landlocked tasks. */+parent_subject=landlock_cred(current_cred());+if(!parent_subject)+return0;++scoped_guard(rcu)+{+child_dom=landlock_get_task_domain(child);+err=domain_ptrace(parent_subject->domain,child_dom);+}++/*+*Fortheptrace_access_checkcase,welogthecurrent/parentdomain+*andthechildtask.+*/+if(err&&!(mode&PTRACE_MODE_NOAUDIT)){+request.layer_plus_one=parent_subject->domain->num_layers;+landlock_log_denial(parent_subject,&request);+}++returnerr;}/**
@@ -110,7 +131,36 @@ static int hook_ptrace_access_check(struct task_struct *const child,*/staticinthook_ptrace_traceme(structtask_struct*constparent){-returntask_ptrace(parent,current);+conststructlandlock_cred_security*parent_subject;+conststructlandlock_ruleset*child_dom;+structlandlock_requestrequest={+.type=LANDLOCK_REQUEST_PTRACE,+.audit={+.type=LSM_AUDIT_DATA_TASK,+.u.tsk=parent,+},+};+interr;++child_dom=landlock_get_current_domain();++guard(rcu)();+parent_subject=landlock_cred(__task_cred(parent));+err=domain_ptrace(parent_subject->domain,child_dom);++/*+*Fortheptrace_tracemecase,welogthedomainwhichisthecauseof+*thedenial,whichmeanstheparentdomaininsteadofthecurrent+*domain.Thismaylookweirdbecausetheptrace_tracemeactionisa+*requesttobetraced,butthesemanticisconsistentwith+*hook_ptrace_access_check().+*/+if(err){+request.layer_plus_one=parent_subject->domain->num_layers;+landlock_log_denial(parent_subject,&request);+}++returnerr;}/**
@@ -129,7 +179,7 @@ static bool domain_is_scoped(const struct landlock_ruleset *const client,access_mask_tscope){intclient_layer,server_layer;-structlandlock_hierarchy*client_walker,*server_walker;+conststructlandlock_hierarchy*client_walker,*server_walker;/* Quick return if client has no domain */if(WARN_ON_ONCE(!client))
Asynchronously log domain information when it first denies an access.
This minimize the amount of generated logs, which makes it possible to
always log denials since they should not happen (except with the new
LANDLOCK_RESTRICT_SELF_QUIET flag). These records are identified with
the new AUDIT_LANDLOCK_DOMAIN type.
The AUDIT_LANDLOCK_DOMAIN message contains:
- the "domain" ID which is described;
- the "status" which can either be "allocated" or "deallocated";
- the "mode" which is for now only "enforcing";
- for the "allocated" status, a minimal set of properties to easily
identify the task that loaded the domain's policy with
landlock_restrict_self(2): "pid", "uid", executable path ("exe"), and
command line ("comm");
- for the "deallocated" state, the number of "denials" accounted to this
domain, which is at least 1.
This requires each domain to save these task properties at creation
time in the new struct landlock_details. A reference to the PID is kept
for the lifetime of the domain to avoid race conditions when
investigating the related task. The executable path is resolved and
stored to not keep a reference to the filesystem and block related
actions. All these metadata are stored for the lifetime of the related
domain and should then be minimal. The required memory is not accounted
to the task calling landlock_restrict_self(2) contrary to most other
Landlock allocations (see related comment).
The AUDIT_LANDLOCK_DOMAIN record follows the first AUDIT_LANDLOCK_ACCESS
record for the same domain, which is always followed by AUDIT_SYSCALL
and AUDIT_PROCTITLE. This is in line with the audit logic to first
record the cause of an event, and then add context with other types of
record.
Audit event sample for a first denial:
type=LANDLOCK_ACCESS msg=audit(1732186800.349:44): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd"
type=LANDLOCK_DOMAIN msg=audit(1732186800.349:44): domain=195ba459b status=allocated mode=enforcing pid=300 uid=0 exe="/root/sandboxer" comm="sandboxer"
type=SYSCALL msg=audit(1732186800.349:44): arch=c000003e syscall=101 success=no [...] pid=300 auid=0
Audit event sample for a following denial:
type=LANDLOCK_ACCESS msg=audit(1732186800.372:45): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd"
type=SYSCALL msg=audit(1732186800.372:45): arch=c000003e syscall=101 success=no [...] pid=300 auid=0
Log domain deletion with the "deallocated" state when a domain was
previously logged. This makes it possible for log parsers to free
potential resources when a domain ID will never show again.
The number of denied access requests is useful to easily check how many
access requests a domain blocked and potentially if some of them are
missing in logs because of audit rate limiting or audit rules. Rate
limiting could also drop this record though.
Audit event sample for a deletion of a domain that denied something:
type=LANDLOCK_DOMAIN msg=audit(1732186800.393:46): domain=195ba459b status=deallocated denials=2
Cc: Günther Noack <gnoack@google.com>
Cc: Paul Moore <paul@paul-moore.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-11-mic@digikod.net
---
Changes since v4:
- Rename AUDIT_LANDLOCK_DOM_{INFO,DROP} to AUDIT_LANDLOCK_DOMAIN and add
a "status" field, as requested by Paul.
- Add a harcoded "mode=enforcing" to leave room for a potential future
permissive mode, as suggested by Paul.
- Remove the "creation" timestamp, as suggested by Paul.
- Move LANDLOCK_PATH_MAX_SIZE to domain.h, check the size of the
greatest landlock_details at build time, and improve comments.
- Improve audit check in landlock_log_drop_domain().
- Add missing headers.
- Fix typo in comment.
- Rebase on top of the landlock_log_denial() and subject type changes.
Changes since v3:
- Log number of denied access requests with AUDIT_LANDLOCK_DOM_DROP
records, suggested by Tyler.
- Do not store a struct path pointer but the resolved string instead.
This enables us to not block unmount of the initially restricted task
executable's mount point. See the new get_current_info() and
get_current_exe(). A following patch add tests for this case.
- Create and allocate a new struct landlock_details for initially
restricted task's information.
- Remove audit_get_ctime() call, as requested by Paul. We now always
have a standalone timestamp per Landlock domain creations.
- Fix docstring.
Changes since v2:
- Fix docstring.
- Fix log_status check in log_hierarchy() to also log
LANDLOCK_LOG_DISABLED.
- Add audit's creation time to domain's properties.
- Use hexadecimal notation for domain IDs.
- Remove domain's parent records: parent domains are not really useful
in the logs. They will be available with the upcoming introspection
feature though.
- Extend commit message with audit's timestamp explanation.
Changes since v1:
- Add a ruleset's version for atomic logs.
- Rebased on the TCP patch series.
- Rename operation using "_" instead of "-".
- Rename AUDIT_LANDLOCK to AUDIT_LANDLOCK_RULESET.
- Only log when audit is enabled, but always set domain IDs.
- Don't log task's PID/TID with log_task() because it would be redundant
with the SYSCALL record.
- Remove race condition when logging ruleset creation and logging
ruleset modification while the related file descriptor was already
registered but the ruleset creation not logged yet.
- Fix domain drop logs.
- Move the domain drop record from the previous patch into this one.
- Do not log domain creation but log first domain use instead.
- Save task's properties that sandbox themselves.
---
include/uapi/linux/audit.h | 1 +
security/landlock/audit.c | 90 ++++++++++++++++++++++++++++++--
security/landlock/audit.h | 7 +++
security/landlock/domain.c | 101 ++++++++++++++++++++++++++++++++++++
security/landlock/domain.h | 68 ++++++++++++++++++++++++
security/landlock/ruleset.c | 6 +++
6 files changed, 270 insertions(+), 3 deletions(-)
@@ -106,16 +142,24 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,if(!is_valid_request(request))return;-if(!unlikely(audit_context()&&audit_enabled))-return;-youngest_layer=request->layer_plus_one-1;youngest_denied=get_hierarchy(subject->domain,youngest_layer);+/*+*Consistentlykeepstrackofthenumberofdeniedaccessrequests+*evenifauditiscurrentlydisabled,ifauditrulescurrently+*excludethisrecordtype,oriflandlock_restrict_self(2)'sflags+*quietlogs.+*/+atomic64_inc(&youngest_denied->num_denials);+/* Ignores denials after an execution. */if(!(subject->domain_exec&(1<<youngest_layer)))return;+if(!unlikely(audit_context()&&audit_enabled))+return;+ab=audit_log_start(audit_context(),GFP_ATOMIC|__GFP_NOWARN,AUDIT_LANDLOCK_ACCESS);if(!ab)
@@ -125,6 +169,46 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,log_blockers(ab,request->type);audit_log_lsm_data(ab,&request->audit);audit_log_end(ab);++/* Logs this domain if it is the first time. */+log_node(youngest_denied);+}++/**+*landlock_log_drop_domain-Createanauditrecordwhenadomainisdeleted+*+*@domain:Thedomainbeingdeleted.+*+*Onlydomainswhichpreviouslyappearedintheauditlogsareloggedagain.+*Thisisusefultoknowwhenadomainwillnevershowagainintheauditlog.+*+*Thisrecordisnotdirectlytiedtoasyscallentry.+*+*Calledbythecred_free()hook,inanuninterruptiblecontext.+*/+voidlandlock_log_drop_domain(conststructlandlock_ruleset*constdomain)+{+structaudit_buffer*ab;++if(WARN_ON_ONCE(!domain->hierarchy))+return;++if(!unlikely(audit_enabled))+return;++/* Ignores domains that were not logged. */+if(READ_ONCE(domain->hierarchy->log_status)!=LANDLOCK_LOG_RECORDED)+return;++ab=audit_log_start(audit_context(),GFP_ATOMIC,+AUDIT_LANDLOCK_DOMAIN);+if(!ab)+return;++audit_log_format(ab,"domain=%llx status=deallocated denials=%llu",+domain->hierarchy->id,+atomic64_read(&domain->hierarchy->num_denials));+audit_log_end(ab);}#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
@@ -10,8 +10,61 @@#ifndef _SECURITY_LANDLOCK_DOMAIN_H#define _SECURITY_LANDLOCK_DOMAIN_H+#include<linux/cred.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>++enumlandlock_log_status{+LANDLOCK_LOG_PENDING=0,+LANDLOCK_LOG_RECORDED,+};++/**+*structlandlock_details-Domain'screationinformation+*+*Rarelyaccessed,mainlywhenloggingthefirstdomain'sdenial.+*+*Thecontainedpointersareinitializedatthedomaincreationtimeandnever+*changedagain.ContrarytomostotherLandlockobjecttypes,thisoneis+*notallocatedwithGFP_KERNEL_ACCOUNTbecauseitssizemaynotbeunderthe+*caller'scontrol(e.g.unknownexe_path)andthedataisnotexplicitly+*requestednorusedbytasks.+*/+structlandlock_details{+/**+*@cred:Credentialofthetaskthatinitiallyrestricteditself,at+*creationtime.+*/+conststructcred*cred;+/**+*@pid:PIDofthetaskthatinitiallyrestricteditself.Itstill+*identifiesthesametask.+*/+structpid*pid;+/**+*@comm:Commandlineofthetaskthatinitiallyrestricteditself,at+*creationtime.AlwaysNULLterminated.+*/+charcomm[TASK_COMM_LEN];+/**+*@exe_path:Executablepathofthetaskthatinitiallyrestricted+*itself,atcreationtime.AlwaysNULLterminated,andnevergreater+*thanLANDLOCK_PATH_MAX_SIZE.+*/+charexe_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(structlandlock_details,exe_path,+LANDLOCK_PATH_MAX_SIZE)<=KMALLOC_MAX_SIZE);/***structlandlock_hierarchy-Nodeinadomainhierarchy
Add audit support for sb_mount, move_mount, sb_umount, sb_remount, and
sb_pivot_root hooks.
The new related blocker is "fs.change_layout".
Audit event sample:
type=LANDLOCK_DENY msg=audit(1729738800.349:44): domain=195ba459b blockers=fs.change_layout name="/" dev="tmpfs" ino=1
Remove landlock_get_applicable_domain() and get_current_fs_domain()
which are now fully replaced with landlock_get_applicable_subject().
Cc: Günther Noack <gnoack@google.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-12-mic@digikod.net
---
Changes since v4:
- Rebase on top of the landlock_log_denial() and subject type changes.
- Fix off-by-one error in landlock_match_layer_level(), now merged into
landlock_get_applicable_domain().
Changes since v3:
- Cosmetic change to the "fs.change_layout" name.
Changes since v2:
- Log the domain that denied the action because not all layers block FS
layout changes.
- Fix landlock_match_layer_level().
Changes since v1:
- Rebased on the TCP patch series.
- Don't log missing permissions, only domain layer, and then remove the
permission word (suggested by Günther)
---
security/landlock/audit.c | 3 ++
security/landlock/audit.h | 1 +
security/landlock/fs.c | 85 ++++++++++++++++++++++++++++++++-----
security/landlock/ruleset.h | 30 -------------
4 files changed, 78 insertions(+), 41 deletions(-)
Add audit support for path_mkdir, path_mknod, path_symlink, path_unlink,
path_rmdir, path_truncate, path_link, path_rename, and file_open hooks.
The dedicated blockers are:
- fs.execute
- fs.write_file
- fs.read_file
- fs.read_dir
- fs.remove_dir
- fs.remove_file
- fs.make_char
- fs.make_dir
- fs.make_reg
- fs.make_sock
- fs.make_fifo
- fs.make_block
- fs.make_sym
- fs.refer
- fs.truncate
- fs.ioctl_dev
Audit event sample for a denied link action:
type=LANDLOCK_DENY msg=audit(1729738800.349:44): domain=195ba459b blockers=fs.refer path="/usr/bin" dev="vda2" ino=351
type=LANDLOCK_DENY msg=audit(1729738800.349:44): domain=195ba459b blockers=fs.make_reg,fs.refer path="/usr/local" dev="vda2" ino=365
We could pack blocker names (e.g. "fs:make_reg,refer") but that would
increase complexity for the kernel and log parsers. Moreover, this
could not handle blockers of different classes (e.g. fs and net). Make
it simple and flexible instead.
Add KUnit tests to check the identification from a layer_mask_t array of
the first layer level denying such request.
Cc: Günther Noack <gnoack@google.com>
Depends-on: 058518c20920 ("landlock: Align partial refer access checks with final ones")
Depends-on: d617f0d72d80 ("landlock: Optimize file path walks and prepare for audit support")
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-13-mic@digikod.net
---
Changes since v4:
- Rebase on top of the landlock_log_denial() and subject type changes.
- Add Depends-on tags.
Changes since v3:
- Rename blockers from fs_* to fs.*
- Extend commit message.
Changes since v2:
- Replace integer with bool in log_blockers().
- Always initialize youngest_layer, spotted by Francis Laniel.
- Fix incorrect log reason by using access_masked_parent1 instead of
access_request_parent1 (thanks to the previous fix patches).
- Clean up formatting.
Changes since v1:
- Move audit code to the ptrace patch.
- Revamp logging and support the path_link and path_rename hooks.
- Add KUnit tests.
---
security/landlock/audit.c | 178 ++++++++++++++++++++++++++++++++++++--
security/landlock/audit.h | 9 ++
security/landlock/fs.c | 62 +++++++++++--
3 files changed, 233 insertions(+), 16 deletions(-)
@@ -145,8 +291,25 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,if(!is_valid_request(request))return;-youngest_layer=request->layer_plus_one-1;-youngest_denied=get_hierarchy(subject->domain,youngest_layer);+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{+/* This will change with the next commit. */+WARN_ON_ONCE(1);+youngest_layer=subject->domain->num_layers;+}+youngest_denied=+get_hierarchy(subject->domain,youngest_layer);+}else{+youngest_layer=request->layer_plus_one-1;+youngest_denied=+get_hierarchy(subject->domain,youngest_layer);+}/**Consistentlykeepstrackofthenumberofdeniedaccessrequests
@@ -1110,6 +1136,7 @@ static int current_check_refer_path(struct dentry *const old_dentry,structdentry*old_parent;layer_mask_tlayer_masks_parent1[LANDLOCK_NUM_ACCESS_FS]={},layer_masks_parent2[LANDLOCK_NUM_ACCESS_FS]={};+structlandlock_requestrequest1={},request2={};if(!subject)return0;
@@ -1141,10 +1168,13 @@ static int current_check_refer_path(struct dentry *const old_dentry,subject->domain,access_request_parent1|access_request_parent2,&layer_masks_parent1,LANDLOCK_KEY_INODE);-if(is_access_to_paths_allowed(-subject->domain,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))return0;++landlock_log_denial(subject,&request1);return-EACCES;}
@@ -1183,10 +1213,20 @@ static int current_check_refer_path(struct dentry *const old_dentry,*/if(is_access_to_paths_allowed(subject->domain,&mnt_dir,access_request_parent1,-&layer_masks_parent1,old_dentry,access_request_parent2,-&layer_masks_parent2,exchange?new_dentry:NULL))+&layer_masks_parent1,&request1,old_dentry,+access_request_parent2,&layer_masks_parent2,&request2,+exchange?new_dentry:NULL))return0;+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);+}+/**ThisprioritizesEACCESoverEXDEVforallactions,including*renameswithRENAME_EXCHANGE.
@@ -1580,6 +1620,7 @@ static int hook_file_open(struct file *const file)optional_access;conststructlandlock_cred_security*constsubject=landlock_get_applicable_subject(file->f_cred,any_fs,NULL);+structlandlock_requestrequest={};if(!subject)return0;
@@ -1606,7 +1647,7 @@ static int hook_file_open(struct file *const file)landlock_init_layer_masks(subject->domain,full_access_request,&layer_masks,LANDLOCK_KEY_INODE),-&layer_masks,NULL,0,NULL,NULL)){+&layer_masks,&request,NULL,0,NULL,NULL,NULL)){allowed_access=full_access_request;}else{unsignedlongaccess_bit;
@@ -1636,6 +1677,9 @@ static int hook_file_open(struct file *const file)if((open_access_request&allowed_access)==open_access_request)return0;+/* Sets access to reflect the actual request. */+request.access=open_access_request;+landlock_log_denial(subject,&request);return-EACCES;}
Add audit support to the file_truncate and file_ioctl hooks.
Add a deny_masks_t type and related helpers to store the domain's layer
level per optional access rights (i.e. LANDLOCK_ACCESS_FS_TRUNCATE and
LANDLOCK_ACCESS_FS_IOCTL_DEV) when opening a file, which cannot be
inferred later. In practice, the landlock_file_security blob size is
unchanged because this new one-byte deny_masks field follows the
existing two-bytes allowed_access field.
Implementing deny_masks_t with a bitfield instead of a struct enables a
generic implementation to store and extract layer levels.
Add KUnit tests to check the identification of a layer level from a
deny_masks_t, and the computation of a deny_masks_t from an access right
with its layer level or a layer_mask_t array.
Audit event sample:
type=LANDLOCK_DENY msg=audit(1729738800.349:44): domain=195ba459b blockers=fs.ioctl_dev path="/dev/tty" dev="devtmpfs" ino=9
Cc: Günther Noack <gnoack@google.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-14-mic@digikod.net
---
Changes since v4:
- Rebase on top of the landlock_log_denial() and subject type changes.
Changes since v3:
- Rename get_layer_from_deny_masks().
Changes since v2:
- Fix !CONFIG_AUDIT build warning.
- Rename ACCESS_FS_OPTIONAL to _LANDLOCK_ACCESS_FS_OPTIONAL.
---
security/landlock/access.h | 23 +++++++
security/landlock/audit.c | 102 ++++++++++++++++++++++++++--
security/landlock/audit.h | 4 ++
security/landlock/domain.c | 133 +++++++++++++++++++++++++++++++++++++
security/landlock/domain.h | 8 +++
security/landlock/fs.c | 51 ++++++++++++++
security/landlock/fs.h | 9 +++
7 files changed, 325 insertions(+), 5 deletions(-)
@@ -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 */+typedefu16access_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);+/*+*Tracksdomainsresponsibleofadeniedaccess.Thisisrequiredtoavoid+*storingineachobjectthefulllayer_masks[]requiredbyupdate_request().+*/+typedefu8deny_masks_t;++/*+*Makessurealloptionalaccessrightscanbetiedtoalayerindex(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. */staticinlinestructaccess_maskslandlock_upgrade_handled_access_masks(structaccess_masksaccess_masks)
@@ -251,22 +253,111 @@ static void test_get_denied_layer(struct kunit *const test)#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */+staticsize_t+get_layer_from_deny_masks(access_mask_t*constaccess_request,+constaccess_mask_tall_existing_optional_access,+constdeny_masks_tdeny_masks)+{+constunsignedlongaccess_opt=all_existing_optional_access;+constunsignedlongaccess_req=*access_request;+access_mask_tmissing=0;+size_tyoungest_layer=0;+size_taccess_index=0;+unsignedlongaccess_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)){+constsize_tlayer=+(deny_masks>>(access_index*4))&+(LANDLOCK_MAX_NUM_LAYERS-1);++if(layer>youngest_layer){+youngest_layer=layer;+missing=BIT(access_bit);+}elseif(layer==youngest_layer){+missing|=BIT(access_bit);+}+}+access_index++;+}++*access_request=missing;+returnyoungest_layer;+}++#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST++staticvoidtest_get_layer_from_deny_masks(structkunit*consttest)+{+deny_masks_tdeny_mask;+access_mask_taccess;++/* 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 */+staticboolis_valid_request(conststructlandlock_request*constrequest){if(WARN_ON_ONCE(!(!!request->layer_plus_one^!!request->access)))returnfalse;if(request->access){-if(WARN_ON_ONCE(!request->layer_masks))+if(WARN_ON_ONCE(!(!!request->layer_masks^+!!request->all_existing_optional_access)))returnfalse;}else{-if(WARN_ON_ONCE(request->layer_masks))+if(WARN_ON_ONCE(request->layer_masks||+request->all_existing_optional_access))returnfalse;}if(WARN_ON_ONCE(!!request->layer_masks^!!request->layer_masks_size))returnfalse;+if(request->deny_masks){+if(WARN_ON_ONCE(!request->all_existing_optional_access))+returnfalse;+}+returntrue;}
@@ -299,9 +390,9 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,subject->domain,&missing,request->layer_masks,request->layer_masks_size);}else{-/* This will change with the next commit. */-WARN_ON_ONCE(1);-youngest_layer=subject->domain->num_layers;+youngest_layer=get_layer_from_deny_masks(+&missing,request->all_existing_optional_access,+request->deny_masks);}youngest_denied=get_hierarchy(subject->domain,youngest_layer);
@@ -383,6 +474,7 @@ 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 */};
@@ -126,4 +131,132 @@ int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)return0;}+staticdeny_masks_t+get_layer_deny_mask(constaccess_mask_tall_existing_optional_access,+constunsignedlongaccess_bit,constsize_tlayer)+{+unsignedlongaccess_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))+return0;++access_weight=hweight_long(all_existing_optional_access&+GENMASK(access_bit,0));+if(WARN_ON_ONCE(access_weight<1))+return0;++returnlayer+<<((access_weight-1)*HWEIGHT(LANDLOCK_MAX_NUM_LAYERS-1));+}++#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST++staticvoidtest_get_layer_deny_mask(structkunit*consttest)+{+constunsignedlongtruncate=BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE);+constunsignedlongioctl_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(constaccess_mask_tall_existing_optional_access,+constaccess_mask_toptional_access,+constlayer_mask_t(*constlayer_masks)[],+constsize_tlayer_masks_size)+{+constunsignedlongaccess_opt=optional_access;+unsignedlongaccess_bit;+deny_masks_tdeny_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))+return0;++if(WARN_ON_ONCE(!access_opt))+return0;++for_each_set_bit(access_bit,&access_opt,layer_masks_size){+constlayer_mask_tmask=(*layer_masks)[access_bit];++if(!mask)+continue;++/* __fls(1) == 0 */+deny_masks|=get_layer_deny_mask(all_existing_optional_access,+access_bit,__fls(mask));+}+returndeny_masks;+}++#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST++staticvoidtest_landlock_get_deny_masks(structkunit*consttest)+{+constlayer_mask_tlayers1[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++staticstructkunit_casetest_cases[]={+/* clang-format off */+KUNIT_CASE(test_get_layer_deny_mask),+KUNIT_CASE(test_landlock_get_deny_masks),+{}+/* clang-format on */+};++staticstructkunit_suitetest_suite={+.name="landlock_domain",+.test_cases=test_cases,+};++kunit_test_suite(test_suite);++#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */+#endif /* CONFIG_AUDIT */
Most of the time we want to log denied access because they should not
happen and such information helps diagnose issues. However, when
sandboxing processes that we know will try to access denied resources
(e.g. unknown, bogus, or malicious binary), we might want to not log
related access requests that might fill up logs.
To disable any log for a specific Landlock domain, add a
LANDLOCK_RESTRICT_SELF_QUIET optional flag to the
landlock_restrict_self() system call.
Because this flag is set for a specific Landlock domain, it makes it
possible to selectively mask some access requests that would be logged
by a parent domain, which might be handy for unprivileged processes to
limit logs. However, system administrators should still use the audit
filtering mechanism.
There is intentionally no audit nor sysctl configuration to re-enable
these quiet domains. This is delegated to the user space program.
Increment the Landlock ABI version to reflect this interface change.
Cc: Günther Noack <gnoack@google.com>
Cc: Paul Moore <paul@paul-moore.com>
Closes: https://github.com/landlock-lsm/linux/issues/3
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-17-mic@digikod.net
---
Using "mute" instead of "quiet" might be more appropriate.
Changes since v4:
- Rebase on top of the scoped guard patches.
Changes since v3:
- Rename LANDLOCK_RESTRICT_SELF_LOGLESS to LANDLOCK_RESTRICT_SELF_QUIET.
"quiet" is already used by kernel's cmdline to disable most log
messages, so this name makes sense for Landlock.
- Improve the LANDLOCK_ABI_VERSION comment.
Changes since v2:
- Update ABI version test.
---
Documentation/userspace-api/landlock.rst | 2 +-
include/uapi/linux/landlock.h | 14 ++++++++++
security/landlock/audit.c | 3 ++
security/landlock/domain.h | 1 +
security/landlock/limits.h | 4 +++
security/landlock/syscalls.c | 29 ++++++++++++++++----
tools/testing/selftests/landlock/base_test.c | 2 +-
7 files changed, 48 insertions(+), 7 deletions(-)
@@ -8,7 +8,7 @@ Landlock: unprivileged access control =====================================:Author: Mickaël Salaün-:Date: October 2024+:Date: January 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
@@ -467,10 +478,12 @@ 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;+is_quiet=!!(flags&LANDLOCK_RESTRICT_SELF_QUIET);+/* Gets and checks the ruleset. */ruleset=get_ruleset_from_fd(ruleset_fd,FMODE_CAN_READ);if(IS_ERR(ruleset))
@@ -493,6 +506,12 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,returnPTR_ERR(new_dom);}+if(is_quiet){+#ifdef CONFIG_AUDIT+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;
@@ -264,16 +264,31 @@ static int hook_unix_stream_connect(struct sock *const sock,structsock*constother,structsock*constnewsk){+size_thandle_layer;conststructlandlock_cred_security*constsubject=landlock_get_applicable_subject(current_cred(),unix_scope,-NULL);+&handle_layer);+structlsm_network_auditaudit_net={+.sk=other,+};+structlandlock_requestrequest={+.type=LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,+.audit={+.type=LSM_AUDIT_DATA_NET,+.u.net=&audit_net,+},+};/* Quick return for non-landlocked tasks. */if(!subject)return0;-if(is_abstract_socket(other)&&sock_is_scoped(other,subject->domain))+if(is_abstract_socket(other)&&+sock_is_scoped(other,subject->domain)){+request.layer_plus_one=handle_layer+1;+landlock_log_denial(subject,&request);return-EPERM;+}return0;}
@@ -281,9 +296,20 @@ static int hook_unix_stream_connect(struct sock *const sock,staticinthook_unix_may_send(structsocket*constsock,structsocket*constother){+size_thandle_layer;conststructlandlock_cred_security*constsubject=landlock_get_applicable_subject(current_cred(),unix_scope,-NULL);+&handle_layer);+structlsm_network_auditaudit_net={+.sk=other->sk,+};+structlandlock_requestrequest={+.type=LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,+.audit={+.type=LSM_AUDIT_DATA_NET,+.u.net=&audit_net,+},+};if(!subject)return0;
@@ -296,8 +322,11 @@ static int hook_unix_may_send(struct socket *const sock,return0;if(is_abstract_socket(other->sk)&&-sock_is_scoped(other->sk,subject->domain))+sock_is_scoped(other->sk,subject->domain)){+request.layer_plus_one=handle_layer+1;+landlock_log_denial(subject,&request);return-EPERM;+}return0;}
@@ -311,13 +340,22 @@ static int hook_task_kill(struct task_struct *const p,conststructcred*cred){boolis_scoped;+size_thandle_layer;conststructlandlock_cred_security*subject;+structlandlock_requestrequest={+.type=LANDLOCK_REQUEST_SCOPE_SIGNAL,+.audit={+.type=LSM_AUDIT_DATA_TASK,+.u.tsk=p,+},+};if(!cred)/* Not dealing with USB IO. */cred=current_cred();-subject=landlock_get_applicable_subject(cred,signal_scope,NULL);+subject=landlock_get_applicable_subject(cred,signal_scope,+&handle_layer);/* Quick return for non-landlocked tasks. */if(!subject)
@@ -329,8 +367,11 @@ static int hook_task_kill(struct task_struct *const p,landlock_get_task_domain(p),signal_scope.scope);}-if(is_scoped)+if(is_scoped){+request.layer_plus_one=handle_layer+1;+landlock_log_denial(subject,&request);return-EPERM;+}return0;}
@@ -338,7 +379,15 @@ static int hook_task_kill(struct task_struct *const p,staticinthook_file_send_sigiotask(structtask_struct*tsk,structfown_struct*fown,intsignum){+size_thandle_layer;conststructlandlock_cred_security*subject;+structlandlock_requestrequest={+.type=LANDLOCK_REQUEST_SCOPE_SIGNAL,+.audit={+.type=LSM_AUDIT_DATA_TASK,+.u.tsk=tsk,+},+};boolis_scoped=false;/* Lock already held by send_sigio() and send_sigurg(). */
@@ -361,8 +410,11 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,landlock_get_task_domain(tsk),signal_scope.scope);}-if(is_scoped)+if(is_scoped){+request.layer_plus_one=handle_layer+1;+landlock_log_denial(subject,&request);return-EPERM;+}return0;}
Log denied access for processes resulting from an execve(2), which is
not the case by default.
The rationale is that a program should know its own behavior, but not
necessarily the behavior of other programs.
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-19-mic@digikod.net
---
Changes since v4:
- New patch to replace the now-removed Landlock-specific audit rule
types.
---
include/uapi/linux/landlock.h | 6 ++++++
security/landlock/audit.c | 3 ++-
security/landlock/domain.c | 1 +
security/landlock/domain.h | 8 +++++++-
security/landlock/limits.h | 2 +-
security/landlock/syscalls.c | 10 +++++++++-
6 files changed, 26 insertions(+), 4 deletions(-)
By default, denials from within the sandbox are not logged. Indeed, the
sandboxer's security policy might not be fitted to the set of sandboxed
processes that could be spawned (e.g. from a shell).
For test purpose, parse the LL_FORCE_LOG environment variable to log
every sandbox denials, including after launching the initial sandboxed
program thanks to LANDLOCK_RESTRICT_SELF_LOG_CROSS_EXEC.
Cc: Günther Noack <gnoack@google.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-20-mic@digikod.net
---
Changes since v3:
- Extend error message, suggested by Francis Laniel.
Changes since v2:
- New patch.
---
samples/landlock/sandboxer.c | 37 +++++++++++++++++++++++++++++++++---
1 file changed, 34 insertions(+), 3 deletions(-)
@@ -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)constchar*cmd_path;char*const*cmd_argv;intruleset_fd,abi;-char*env_port_name;+char*env_port_name,*env_force_log;__u64access_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,};+intsupported_restrict_flags=LANDLOCK_RESTRICT_SELF_LOG_CROSS_EXEC;+intset_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));+case6:+/* Removes LANDLOCK_RESTRICT_SELF_LOG_CROSS_EXEC for ABI < 7 */+supported_restrict_flags&=+~LANDLOCK_RESTRICT_SELF_LOG_CROSS_EXEC;++/* 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))return1;+/* 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");+return1;+}+if(!(supported_restrict_flags&+LANDLOCK_RESTRICT_SELF_LOG_CROSS_EXEC)){+fprintf(stderr,+"Audit logs not supported by current kernel\n");+return1;+}+set_restrict_flags|=LANDLOCK_RESTRICT_SELF_LOG_CROSS_EXEC;+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");gotoerr_close_ruleset;}-if(landlock_restrict_self(ruleset_fd,0)){+if(landlock_restrict_self(ruleset_fd,set_restrict_flags)){perror("Failed to enforce ruleset");gotoerr_close_ruleset;}
Add LANDLOCK_RESTRICT_SELF_QUIET_SUBDOMAINS for the case of sandboxer
tools, init systems, or runtime containers launching programs sandboxing
themselves in an inconsistent way. Setting this flag should only
depends on runtime configuration (i.e. not hardcoded).
We don't create a new ruleset's option because this should not be part
of the security policy: only the task that enforces the policy (not the
one that create it) knows if itself or its children may request denied
actions.
Cc: Günther Noack <gnoack@google.com>
Cc: Paul Moore <paul@paul-moore.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-18-mic@digikod.net
---
Using "mute" instead of "quiet" might be more appropriate.
Changes since v4:
- New patch.
---
include/uapi/linux/landlock.h | 11 +++++++++++
security/landlock/domain.c | 1 +
security/landlock/domain.h | 5 +++++
security/landlock/limits.h | 2 +-
security/landlock/syscalls.c | 14 +++++++++++---
5 files changed, 29 insertions(+), 4 deletions(-)
Add the restrict_self_flags test suite to check that
LANDLOCK_RESTRICT_SELF_QUIET, LANDLOCK_RESTRICT_SELF_QUIET_SUBDOMAINS,
and LANDLOCK_RESTRICT_SELF_LOG_CROSS_EXEC are valid but not the next
bit. Also test flags incompatibility. Some checks are similar to
restrict_self_checks_ordering's ones.
Cc: Günther Noack <gnoack@google.com>
Cc: Paul Moore <paul@paul-moore.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-21-mic@digikod.net
---
Changes since v4:
- Update with LANDLOCK_RESTRICT_SELF_QUIET_SUBDOMAINS, and
LANDLOCK_RESTRICT_SELF_LOG_CROSS_EXEC.
Changes since v3:
- Use a last_flag variable.
Changes since v2:
- New patch.
---
tools/testing/selftests/landlock/base_test.c | 41 ++++++++++++++++++++
1 file changed, 41 insertions(+)
Add audit_test.c to check with and without LANDLOCK_RESTRICT_SELF_*
flags against the two Landlock audit record types:
AUDIT_LANDLOCK_ACCESS and AUDIT_LANDLOCK_DOMAIN.
Tests are run with audit filters to ensure the audit records come from
the test program. Moreover, because there can only be one audit
process, tests would failed if run in parallel. Because of audit
limitations, tests can only be run in the initial namespace.
The audit test helpers were inspired by libaudit and
tools/testing/selftests/net/netfilter/audit_logread.c
Cc: Günther Noack <gnoack@google.com>
Cc: Paul Moore <paul@paul-moore.com>
Cc: Phil Sutter <phil@nwl.cc>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-22-mic@digikod.net
---
Changes since v4:
- Update with the new landlock_restrict_self()'s flags, the new audit
rule types, and message fields.
- Simplify audit_filter_exe() and audit_init_filter_exe().
- Test with kill() instead of umount().
- Test domain deallocation events.
Changes since v3:
- Improve audit_request() to check Netlink errors and handle multiple
replies.
- Make audit_filter_exe() more generic to handle several audit rule
lists.
- Merge audit_init_state() into audit_init() and create
audit_init_with_exe_filter() to handle AUDIT_EXE_LANDLOCK_DENY with an
arbitrary path.
- Add matches_log_dom_info().
Changes since v2:
- New patch.
---
tools/testing/selftests/landlock/audit.h | 358 ++++++++++++++++++
tools/testing/selftests/landlock/audit_test.c | 204 ++++++++++
tools/testing/selftests/landlock/common.h | 2 +
tools/testing/selftests/landlock/config | 1 +
4 files changed, 565 insertions(+)
create mode 100644 tools/testing/selftests/landlock/audit.h
create mode 100644 tools/testing/selftests/landlock/audit_test.c
@@ -37,6 +38,7 @@ static void _init_caps(struct __test_metadata *const _metadata, bool drop_all)/* Only these three capabilities are useful for the tests. */constcap_value_tcaps[]={/* clang-format off */+CAP_AUDIT_CONTROL,CAP_DAC_OVERRIDE,CAP_MKNOD,CAP_NET_ADMIN,
Add tests for all ptrace actions. This improves all the ptrace tests by
making sure that the restrictions comes from Landlock, and with the
expected objects. These extended tests are like enhanced errno checks
that make sure Landlock enforcement is consistent.
Test coverage for security/landlock is 93.4% of 1619 lines according to
gcc/gcov-14.
Cc: Günther Noack <gnoack@google.com>
Cc: Paul Moore <paul@paul-moore.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-24-mic@digikod.net
---
Changes since v3:
- Update test coverage.
Changes since v2:
- New patch.
---
.../testing/selftests/landlock/ptrace_test.c | 67 +++++++++++++++++--
1 file changed, 63 insertions(+), 4 deletions(-)
@@ -17,6 +18,7 @@#include<sys/wait.h>#include<unistd.h>+#include"audit.h"#include"common.h"/* Copied from security/yama/yama_lsm.c */
@@ -83,9 +85,27 @@ static int get_yama_ptrace_scope(void)returnret;}-/* clang-format off */-FIXTURE(hierarchy){};-/* clang-format on */+staticintmatches_log_ptrace(struct__test_metadata*const_metadata,+intaudit_fd,constpid_topid)+{+staticconstcharlog_template[]=REGEX_LANDLOCK_PREFIX+" blockers=ptrace opid=%d ocomm=\"ptrace_test\"$";+charlog_match[sizeof(log_template)+10];+intlog_match_len;++log_match_len=+snprintf(log_match,sizeof(log_match),log_template,opid);+if(log_match_len>sizeof(log_match))+return-E2BIG;++returnaudit_match_record(audit_fd,AUDIT_LANDLOCK_ACCESS,log_match);+}++FIXTURE(hierarchy)+{+structaudit_filteraudit_filter;+intaudit_fd;+};FIXTURE_VARIANT(hierarchy){
@@ -243,10 +263,16 @@ FIXTURE_VARIANT_ADD(hierarchy, deny_with_forked_domain) {FIXTURE_SETUP(hierarchy){+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(hierarchy)+FIXTURE_TEARDOWN_PARENT(hierarchy){+EXPECT_EQ(0,audit_cleanup(-1,NULL));}/* Test PTRACE_TRACEME and PTRACE_ATTACH for parent and child. */
@@ -334,17 +361,29 @@ TEST_F(hierarchy, trace)err_proc_read=test_ptrace_read(parent);if(can_read_parent){EXPECT_EQ(0,err_proc_read);+EXPECT_EQ(-EAGAIN,+matches_log_ptrace(_metadata,self->audit_fd,+parent));}else{EXPECT_EQ(EACCES,err_proc_read);+EXPECT_EQ(0,+matches_log_ptrace(_metadata,self->audit_fd,+parent));}/* Tests PTRACE_ATTACH on the parent. */ret=ptrace(PTRACE_ATTACH,parent,NULL,0);if(can_trace_parent){EXPECT_EQ(0,ret);+EXPECT_EQ(-EAGAIN,+matches_log_ptrace(_metadata,self->audit_fd,+parent));}else{EXPECT_EQ(-1,ret);EXPECT_EQ(EPERM,errno);+EXPECT_EQ(can_read_parent?-EAGAIN:0,+matches_log_ptrace(_metadata,self->audit_fd,+parent));}if(ret==0){ASSERT_EQ(parent,waitpid(parent,&status,0));
@@ -356,9 +395,16 @@ TEST_F(hierarchy, trace)ret=ptrace(PTRACE_TRACEME);if(can_trace_child){EXPECT_EQ(0,ret);+EXPECT_EQ(-EAGAIN,+matches_log_ptrace(_metadata,self->audit_fd,+parent));}else{EXPECT_EQ(-1,ret);EXPECT_EQ(EPERM,errno);+/* We should indeed see the parent process. */+EXPECT_EQ(can_read_child?-EAGAIN:0,+matches_log_ptrace(_metadata,self->audit_fd,+parent));}/*
@@ -406,17 +452,25 @@ TEST_F(hierarchy, trace)err_proc_read=test_ptrace_read(child);if(can_read_child){EXPECT_EQ(0,err_proc_read);+EXPECT_EQ(-EAGAIN,+matches_log_ptrace(_metadata,self->audit_fd,child));}else{EXPECT_EQ(EACCES,err_proc_read);+EXPECT_EQ(0,+matches_log_ptrace(_metadata,self->audit_fd,child));}/* Tests PTRACE_ATTACH on the child. */ret=ptrace(PTRACE_ATTACH,child,NULL,0);if(can_trace_child){EXPECT_EQ(0,ret);+EXPECT_EQ(-EAGAIN,+matches_log_ptrace(_metadata,self->audit_fd,child));}else{EXPECT_EQ(-1,ret);EXPECT_EQ(EPERM,errno);+EXPECT_EQ(can_read_child?-EAGAIN:0,+matches_log_ptrace(_metadata,self->audit_fd,child));}if(ret==0){
@@ -432,6 +486,11 @@ TEST_F(hierarchy, trace)if(WIFSIGNALED(status)||!WIFEXITED(status)||WEXITSTATUS(status)!=EXIT_SUCCESS)_metadata->exit_code=KSFT_FAIL;++/* Makes sure there is no superfluous logged records. */+audit_count_records(self->audit_fd,&records);+EXPECT_EQ(0,records.access);+EXPECT_EQ(0,records.domain);}TEST_HARNESS_MAIN
Because audit is dedicated to the system administrator, create a new
entry in Documentation/admin-guide/LSM .
Extend other Landlock documentation's pages with this new one.
Cc: Günther Noack <gnoack@google.com>
Cc: Paul Moore <paul@paul-moore.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-25-mic@digikod.net
---
Changes since v4:
- New patch.
---
Documentation/admin-guide/LSM/index.rst | 1 +
Documentation/admin-guide/LSM/landlock.rst | 157 +++++++++++++++++++++
Documentation/security/landlock.rst | 7 +
Documentation/userspace-api/landlock.rst | 7 +
MAINTAINERS | 1 +
5 files changed, 173 insertions(+)
create mode 100644 Documentation/admin-guide/LSM/landlock.rst
@@ -124,6 +124,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/
@@ -683,9 +683,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
Add audit_exec tests to filter Landlock denials according to
cross-execution or muted subdomains.
Add a wait-pipe-sandbox.c test program to sandbox itself and send a
(denied) signals to its parent.
Cc: Günther Noack <gnoack@google.com>
Cc: Paul Moore <paul@paul-moore.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-23-mic@digikod.net
---
Changes since v4:
- Revamp to test the Landlock syscall flags instead of the audit rules.
- Copy wait-pipe.c to wait-pipe-sandbox.c and extend it.
- Fix regex.
Changes since v3:
- New patch.
---
tools/testing/selftests/landlock/Makefile | 6 +-
tools/testing/selftests/landlock/audit_test.c | 221 ++++++++++++++++++
tools/testing/selftests/landlock/common.h | 1 +
.../selftests/landlock/wait-pipe-sandbox.c | 131 +++++++++++
4 files changed, 358 insertions(+), 1 deletion(-)
create mode 100644 tools/testing/selftests/landlock/wait-pipe-sandbox.c
@@ -201,4 +212,214 @@ TEST_F(audit_fork, flags)}}+FIXTURE(audit_exec)+{+structaudit_filteraudit_filter;+intaudit_fd;+};++FIXTURE_VARIANT(audit_exec)+{+constintrestrict_flags;+};++/* clang-format off */+FIXTURE_VARIANT_ADD(audit_exec,default){+/* clang-format on */+.restrict_flags=0,+};++/* clang-format off */+FIXTURE_VARIANT_ADD(audit_exec,quiet){+/* clang-format on */+.restrict_flags=LANDLOCK_RESTRICT_SELF_QUIET,+};++/* clang-format off */+FIXTURE_VARIANT_ADD(audit_exec,quiet_subdomains){+/* clang-format on */+.restrict_flags=LANDLOCK_RESTRICT_SELF_QUIET_SUBDOMAINS,+};++/* clang-format off */+FIXTURE_VARIANT_ADD(audit_exec,log_cross_exec){+/* clang-format on */+.restrict_flags=LANDLOCK_RESTRICT_SELF_LOG_CROSS_EXEC,+};++/* clang-format off */+FIXTURE_VARIANT_ADD(audit_exec,quiet_subdomains_and_log_cross_exec){+/* clang-format on */+.restrict_flags=LANDLOCK_RESTRICT_SELF_QUIET_SUBDOMAINS|+LANDLOCK_RESTRICT_SELF_LOG_CROSS_EXEC,+};++FIXTURE_SETUP(audit_exec)+{+disable_caps(_metadata);+set_cap(_metadata,CAP_AUDIT_CONTROL);++self->audit_fd=audit_init();+EXPECT_LE(0,self->audit_fd)+{+constchar*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,flags)+{+structaudit_recordsrecords;+intpipe_child[2],pipe_parent[2];+charbuf_parent;+pid_tchild;+intstatus;++ASSERT_EQ(0,pipe2(pipe_child,0));+ASSERT_EQ(0,pipe2(pipe_parent,0));++child=fork();+ASSERT_LE(0,child);+if(child==0){+conststructlandlock_ruleset_attrlayer1={+.scoped=LANDLOCK_SCOPE_SIGNAL,+};+charpipe_child_str[12],pipe_parent_str[12];+char*constargv[]={(char*)bin_wait_pipe_sandbox,+pipe_child_str,pipe_parent_str,NULL};+intruleset_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. */+audit_count_records(self->audit_fd,&records);+EXPECT_EQ(0,records.access);+EXPECT_EQ(0,records.domain);++/*+*Waitforthechildtodoafirstdeniedactionbylayer1and+*sandboxitselfwithlayer2.+*/+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_CROSS_EXEC){+/* Matches the current domain. */+EXPECT_EQ(0,matches_log_signal(_metadata,self->audit_fd,+getpid()));+}++/* Checks that we didn't miss anything. */+audit_count_records(self->audit_fd,&records);+EXPECT_EQ(0,records.access);++/*+*Waitforthechildtodoaseconddeniedactionbylayer1and+*layer2,andsandboxitselfwithlayer3.+*/+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_CROSS_EXEC){+/* Matches the current domain. */+EXPECT_EQ(0,matches_log_signal(_metadata,self->audit_fd,+getpid()));+}++if(!(variant->restrict_flags&+LANDLOCK_RESTRICT_SELF_QUIET_SUBDOMAINS)){+/* Matches the child domain. */+EXPECT_EQ(0,+matches_log_fs_read_root(_metadata,self->audit_fd));+}++/* Checks that we didn't miss anything. */+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_QUIET_SUBDOMAINS)){+/*+*Matchesthechilddomains,whichteststhatthe+*llcred->domain_execbitmaskiscorrectlyupdatedwithanew+*domain.+*/+EXPECT_EQ(0,+matches_log_fs_read_root(_metadata,self->audit_fd));+EXPECT_EQ(0,matches_log_signal(_metadata,self->audit_fd,+getpid()));+}++/* Checks that we didn't miss anything. */+audit_count_records(self->audit_fd,&records);+EXPECT_EQ(0,records.access);+}+TEST_HARNESS_MAIN
This cosmetic change that is needed for audit support, specifically to
be able to filter according to cross-execution boundaries.
Replace hardcoded LANDLOCK_SCOPE_SIGNAL with the signal_scope.scope
variable.
Use scoped guards for RCU read-side critical sections.
Cc: Günther Noack <gnoack@google.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-7-mic@digikod.net
---
Changes since v4:
- New patch.
---
security/landlock/task.c | 50 +++++++++++++++++++++-------------------
1 file changed, 26 insertions(+), 24 deletions(-)
@@ -213,15 +214,15 @@ static int hook_unix_stream_connect(struct sock *const sock,structsock*constother,structsock*constnewsk){-conststructlandlock_ruleset*constdom=-landlock_get_applicable_domain(landlock_get_current_domain(),-unix_scope);+conststructlandlock_cred_security*constsubject=+landlock_get_applicable_subject(current_cred(),unix_scope,+NULL);/* Quick return for non-landlocked tasks. */-if(!dom)+if(!subject)return0;-if(is_abstract_socket(other)&&sock_is_scoped(other,dom))+if(is_abstract_socket(other)&&sock_is_scoped(other,subject->domain))return-EPERM;return0;
@@ -230,11 +231,11 @@ static int hook_unix_stream_connect(struct sock *const sock,staticinthook_unix_may_send(structsocket*constsock,structsocket*constother){-conststructlandlock_ruleset*constdom=-landlock_get_applicable_domain(landlock_get_current_domain(),-unix_scope);+conststructlandlock_cred_security*constsubject=+landlock_get_applicable_subject(current_cred(),unix_scope,+NULL);-if(!dom)+if(!subject)return0;/*
@@ -244,7 +245,8 @@ static int hook_unix_may_send(struct socket *const sock,if(unix_peer(sock->sk)==other->sk)return0;-if(is_abstract_socket(other->sk)&&sock_is_scoped(other->sk,dom))+if(is_abstract_socket(other->sk)&&+sock_is_scoped(other->sk,subject->domain))return-EPERM;return0;
@@ -256,27 +258,27 @@ static const struct access_masks signal_scope = {staticinthook_task_kill(structtask_struct*constp,structkernel_siginfo*constinfo,constintsig,-conststructcred*constcred)+conststructcred*cred){boolis_scoped;-conststructlandlock_ruleset*dom;+conststructlandlock_cred_security*subject;-if(cred){-/* Dealing with USB IO. */-dom=landlock_cred(cred)->domain;-}else{-dom=landlock_get_current_domain();-}-dom=landlock_get_applicable_domain(dom,signal_scope);+if(!cred)+/* Not dealing with USB IO. */+cred=current_cred();++subject=landlock_get_applicable_subject(cred,signal_scope,NULL);/* Quick return for non-landlocked tasks. */-if(!dom)+if(!subject)return0;-rcu_read_lock();-is_scoped=domain_is_scoped(dom,landlock_get_task_domain(p),-LANDLOCK_SCOPE_SIGNAL);-rcu_read_unlock();+scoped_guard(rcu)+{+is_scoped=domain_is_scoped(subject->domain,+landlock_get_task_domain(p),+signal_scope.scope);+}if(is_scoped)return-EPERM;
security/landlock/syscalls.c:469:24: warning: variable 'is_quiet_subdomains' set but not used [-Wunused-but-set-variable]
469 | bool is_quiet, is_quiet_subdomains,
| ^~~~~~~~~~~~~~~~~~~
security/landlock/syscalls.c:469:14: warning: variable 'is_quiet' set but not used [-Wunused-but-set-variable]
469 | bool is_quiet, is_quiet_subdomains,
| ^~~~~~~~
vim +/is_quiet_subdomains +469 security/landlock/syscalls.c
435
436 /**
437 * sys_landlock_restrict_self - Enforce a ruleset on the calling thread
438 *
439 * @ruleset_fd: File descriptor tied to the ruleset to merge with the target.
440 * @flags: Supported values:
441 *
442 * - %LANDLOCK_RESTRICT_SELF_QUIET
443 * - %LANDLOCK_RESTRICT_SELF_QUIET_SUBDOMAINS
444 *
445 * This system call enables to enforce a Landlock ruleset on the current
446 * thread. Enforcing a ruleset requires that the task has %CAP_SYS_ADMIN in its
447 * namespace or is running with no_new_privs. This avoids scenarios where
448 * unprivileged tasks can affect the behavior of privileged children.
449 *
450 * Possible returned errors are:
451 *
452 * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
453 * - %EINVAL: @flags contains an unknown bit.
454 * - %EBADF: @ruleset_fd is not a file descriptor for the current thread;
455 * - %EBADFD: @ruleset_fd is not a ruleset file descriptor;
456 * - %EPERM: @ruleset_fd has no read access to the underlying ruleset, or the
457 * current thread is not running with no_new_privs, or it doesn't have
458 * %CAP_SYS_ADMIN in its namespace.
459 * - %E2BIG: The maximum number of stacked rulesets is reached for the current
460 * thread.
461 */
462 SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
463 flags)
464 {
465 struct landlock_ruleset *new_dom,
466 *ruleset __free(landlock_put_ruleset) = NULL;
467 struct cred *new_cred;
468 struct landlock_cred_security *new_llcred;
> 469 bool is_quiet, is_quiet_subdomains,
470 __maybe_unused inherits_quiet_subdomains;
471
472 if (!is_initialized())
473 return -EOPNOTSUPP;
474
475 /*
476 * Similar checks as for seccomp(2), except that an -EPERM may be
477 * returned.
478 */
479 if (!task_no_new_privs(current) &&
480 !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))
481 return -EPERM;
482
483 if ((flags | LANDLOCK_MASK_RESTRICT_SELF) !=
484 LANDLOCK_MASK_RESTRICT_SELF)
485 return -EINVAL;
486
487 is_quiet = !!(flags & LANDLOCK_RESTRICT_SELF_QUIET);
488 is_quiet_subdomains =
489 !!(flags & LANDLOCK_RESTRICT_SELF_QUIET_SUBDOMAINS);
490
491 /* Gets and checks the ruleset. */
492 ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ);
493 if (IS_ERR(ruleset))
494 return PTR_ERR(ruleset);
495
496 /* Prepares new credentials. */
497 new_cred = prepare_creds();
498 if (!new_cred)
499 return -ENOMEM;
500
501 new_llcred = landlock_cred(new_cred);
502
503 /*
504 * There is no possible race condition while copying and manipulating
505 * the current credentials because they are dedicated per thread.
506 */
507 new_dom = landlock_merge_ruleset(new_llcred->domain, ruleset);
508 if (IS_ERR(new_dom)) {
509 abort_creds(new_cred);
510 return PTR_ERR(new_dom);
511 }
512
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
From: Paul Moore <paul@paul-moore.com> Date: 2025-02-14 22:52:49
On Jan 31, 2025 =?UTF-8?q?Micka=C3=ABl=20Sala=C3=BCn?= [off-list ref] wrote:
Add a new AUDIT_LANDLOCK_ACCESS record type dedicated to an access
request denied by a Landlock domain. AUDIT_LANDLOCK_ACCESS indicates
that something unexpected happened.
For now, only denied access are logged, which means that any
AUDIT_LANDLOCK_ACCESS record is always followed by a SYSCALL record with
"success=no". However, log parsers should check this syscall property
because this is the only sign that a request was denied. Indeed, we
could have "success=yes" if Landlock would support a "permissive" mode.
We could also add a new field for this mode to AUDIT_LANDLOCK_DOMAIN
(see following commit).
By default, the only logged access requests are those coming from the
same executed program that enforced the Landlock restriction on itself.
In other words, no audit record are created for a task after it called
execve(2). This is required to avoid log spam because programs may only
be aware of their own restrictions, but not the inherited ones.
Following commits will allow to conditionally generate
AUDIT_LANDLOCK_ACCESS records according to dedicated
landlock_restrict_self(2)'s flags.
The AUDIT_LANDLOCK_ACCESS message contains:
- the "domain" ID restricting the action on an object,
- the "blockers" that are missing to allow the requested access,
- a set of fields identifying the related object (e.g. task identified
with "opid" and "ocomm").
The blockers are implicit restrictions (e.g. ptrace), or explicit access
rights (e.g. filesystem), or explicit scopes (e.g. signal). This field
contains a list of at least one element, each separated with a comma.
The initial blocker is "ptrace", which describe all implicit Landlock
restrictions related to ptrace (e.g. deny tracing of tasks outside a
sandbox).
Add audit support to ptrace_access_check and ptrace_traceme hooks. For
the ptrace_access_check case, we log the current/parent domain and the
child task. For the ptrace_traceme case, we log the parent domain and
the parent task. Indeed, the requester is the current task, but the
action would be performed by the parent task.
Audit event sample:
type=LANDLOCK_ACCESS msg=audit(1729738800.349:44): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd"
type=SYSCALL msg=audit(1729738800.349:44): arch=c000003e syscall=101 success=no [...] pid=300 auid=0
A following commit adds user documentation.
Add KUnit tests to check reading of domain ID relative to layer level.
The quick return for non-landlocked tasks is moved from task_ptrace() to
each LSM hooks.
Because the landlock_log_denial() function is only called when an access
is denied, the compiler should be able to optimize the struct
landlock_request initializations. It is not useful to inline the
audit_enabled check because other computation are performed anyway, and
by the same landlock_log_denia() code.
Use scoped guards for RCU read-side critical sections.
Cc: Günther Noack <gnoack@google.com>
Cc: Paul Moore <paul@paul-moore.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-10-mic@digikod.net
---
Changes since v4:
- Rename AUDIT_LANDLOCK_DENY to AUDIT_LANDLOCK_ACCESS, requested by
Paul.
- Make landlock_log_denial() get Landlock credential instead of Landlock
domain to be able to filter on the domain_exe variable.
- Rebase on top of the migration from struct landlock_ruleset to struct
landlock_cred_security.
- Rename landlock_init_current_hierarchy() to
landlock_init_hierarchy_log().
- Rebase on top of the scoped guard patches.
- By default, do not log denials after an execution.
- Use scoped guards for RCU read-side critical sections.
Changes since v3:
- Extend commit message.
Changes since v2:
- Log domain IDs as hexadecimal number: this is a more compact notation
(i.e. at least one less digit), it improves alignment in logs, and it
makes most IDs start with 1 as leading digit (because of the 2^32
minimal value). Do not use the "0x" prefix that would add useless
data to logs.
- Constify function arguments.
- Clean up Makefile entries.
Changes since v1:
- Move most audit code to this patch.
- Rebase on the TCP patch series.
- Don't log missing access right: simplify and make it generic for rule
types.
- Don't log errno and then don't wrap the error with
landlock_log_request(), as suggested by Jeff.
- Add a WARN_ON_ONCE() check to never dereference null pointers.
- Only log when audit is enabled.
- Don't log task's PID/TID with log_task() because it would be redundant
with the SYSCALL record.
- Move the "op" in front and rename "domain" to "denying_domain" to make
it more consistent with other entries.
- Don't update the request with the domain ID but add an helper to get
it from the layer masks (and in a following commit with a struct
file).
- Revamp get_domain_id_from_layer_masks() into
get_level_from_layer_masks().
- For ptrace_traceme, log the parent domain instead of the current one.
- Add documentation.
- Rename AUDIT_LANDLOCK_DENIAL to AUDIT_LANDLOCK_DENY.
- Only log the domain ID and the target task.
- Log "blockers", which are either implicit restrictions (e.g. ptrace)
or explicit access rights (e.g. filesystem), or scopes (e.g. signal).
- Don't log LSM hook names/operations.
- Pick an audit event ID folling the IPE ones.
- Add KUnit tests.
---
include/uapi/linux/audit.h | 3 +-
security/landlock/Makefile | 5 +-
security/landlock/audit.c | 146 ++++++++++++++++++++++++++++++++++++
security/landlock/audit.h | 53 +++++++++++++
security/landlock/domain.c | 28 +++++++
security/landlock/domain.h | 22 ++++++
security/landlock/ruleset.c | 6 ++
security/landlock/task.c | 96 ++++++++++++++++++------
8 files changed, 334 insertions(+), 25 deletions(-)
create mode 100644 security/landlock/audit.c
create mode 100644 security/landlock/audit.h
create mode 100644 security/landlock/domain.c
Based on previous discussions I'm under the impression that you are
planning to add a Landlock "permissive" mode at some point in the
future and based on the comments above you plan to add a "success="
field to the _ACCESS record defined here. There is no problem with
adding fields to an existing record, but the general guidance is that
new fields need to be added to the end of the record (limitations due
the the audit userspace and poor guidance in the early days of audit).
Assuming you are okay with that there is no need to change anything,
but if you would prefer the "permissive=" field to occur somewhere
else in the record you may want to consider adding a "permissive=no"
now. Otherwise this looks okay from an audit perspective.
[P.S. I just got to patch 10/24 and saw the enforcing field there,
the comments above still stand, but it looks like you chose to note
this in the _DOMAIN record, which is fine.]
Acked-by: Paul Moore <paul@paul-moore.com> (Audit)
--
paul-moore.com
From: Paul Moore <paul@paul-moore.com> Date: 2025-02-14 22:52:51
On Jan 31, 2025 =?UTF-8?q?Micka=C3=ABl=20Sala=C3=BCn?= [off-list ref] wrote:
Asynchronously log domain information when it first denies an access.
This minimize the amount of generated logs, which makes it possible to
always log denials since they should not happen (except with the new
LANDLOCK_RESTRICT_SELF_QUIET flag). These records are identified with
the new AUDIT_LANDLOCK_DOMAIN type.
The AUDIT_LANDLOCK_DOMAIN message contains:
- the "domain" ID which is described;
- the "status" which can either be "allocated" or "deallocated";
- the "mode" which is for now only "enforcing";
- for the "allocated" status, a minimal set of properties to easily
identify the task that loaded the domain's policy with
landlock_restrict_self(2): "pid", "uid", executable path ("exe"), and
command line ("comm");
- for the "deallocated" state, the number of "denials" accounted to this
domain, which is at least 1.
This requires each domain to save these task properties at creation
time in the new struct landlock_details. A reference to the PID is kept
for the lifetime of the domain to avoid race conditions when
investigating the related task. The executable path is resolved and
stored to not keep a reference to the filesystem and block related
actions. All these metadata are stored for the lifetime of the related
domain and should then be minimal. The required memory is not accounted
to the task calling landlock_restrict_self(2) contrary to most other
Landlock allocations (see related comment).
The AUDIT_LANDLOCK_DOMAIN record follows the first AUDIT_LANDLOCK_ACCESS
record for the same domain, which is always followed by AUDIT_SYSCALL
and AUDIT_PROCTITLE. This is in line with the audit logic to first
record the cause of an event, and then add context with other types of
record.
Audit event sample for a first denial:
type=LANDLOCK_ACCESS msg=audit(1732186800.349:44): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd"
type=LANDLOCK_DOMAIN msg=audit(1732186800.349:44): domain=195ba459b status=allocated mode=enforcing pid=300 uid=0 exe="/root/sandboxer" comm="sandboxer"
type=SYSCALL msg=audit(1732186800.349:44): arch=c000003e syscall=101 success=no [...] pid=300 auid=0
Audit event sample for a following denial:
type=LANDLOCK_ACCESS msg=audit(1732186800.372:45): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd"
type=SYSCALL msg=audit(1732186800.372:45): arch=c000003e syscall=101 success=no [...] pid=300 auid=0
Log domain deletion with the "deallocated" state when a domain was
previously logged. This makes it possible for log parsers to free
potential resources when a domain ID will never show again.
The number of denied access requests is useful to easily check how many
access requests a domain blocked and potentially if some of them are
missing in logs because of audit rate limiting or audit rules. Rate
limiting could also drop this record though.
Audit event sample for a deletion of a domain that denied something:
type=LANDLOCK_DOMAIN msg=audit(1732186800.393:46): domain=195ba459b status=deallocated denials=2
Cc: Günther Noack <gnoack@google.com>
Cc: Paul Moore <paul@paul-moore.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-11-mic@digikod.net
---
Changes since v4:
- Rename AUDIT_LANDLOCK_DOM_{INFO,DROP} to AUDIT_LANDLOCK_DOMAIN and add
a "status" field, as requested by Paul.
- Add a harcoded "mode=enforcing" to leave room for a potential future
permissive mode, as suggested by Paul.
- Remove the "creation" timestamp, as suggested by Paul.
- Move LANDLOCK_PATH_MAX_SIZE to domain.h, check the size of the
greatest landlock_details at build time, and improve comments.
- Improve audit check in landlock_log_drop_domain().
- Add missing headers.
- Fix typo in comment.
- Rebase on top of the landlock_log_denial() and subject type changes.
Changes since v3:
- Log number of denied access requests with AUDIT_LANDLOCK_DOM_DROP
records, suggested by Tyler.
- Do not store a struct path pointer but the resolved string instead.
This enables us to not block unmount of the initially restricted task
executable's mount point. See the new get_current_info() and
get_current_exe(). A following patch add tests for this case.
- Create and allocate a new struct landlock_details for initially
restricted task's information.
- Remove audit_get_ctime() call, as requested by Paul. We now always
have a standalone timestamp per Landlock domain creations.
- Fix docstring.
Changes since v2:
- Fix docstring.
- Fix log_status check in log_hierarchy() to also log
LANDLOCK_LOG_DISABLED.
- Add audit's creation time to domain's properties.
- Use hexadecimal notation for domain IDs.
- Remove domain's parent records: parent domains are not really useful
in the logs. They will be available with the upcoming introspection
feature though.
- Extend commit message with audit's timestamp explanation.
Changes since v1:
- Add a ruleset's version for atomic logs.
- Rebased on the TCP patch series.
- Rename operation using "_" instead of "-".
- Rename AUDIT_LANDLOCK to AUDIT_LANDLOCK_RULESET.
- Only log when audit is enabled, but always set domain IDs.
- Don't log task's PID/TID with log_task() because it would be redundant
with the SYSCALL record.
- Remove race condition when logging ruleset creation and logging
ruleset modification while the related file descriptor was already
registered but the ruleset creation not logged yet.
- Fix domain drop logs.
- Move the domain drop record from the previous patch into this one.
- Do not log domain creation but log first domain use instead.
- Save task's properties that sandbox themselves.
---
include/uapi/linux/audit.h | 1 +
security/landlock/audit.c | 90 ++++++++++++++++++++++++++++++--
security/landlock/audit.h | 7 +++
security/landlock/domain.c | 101 ++++++++++++++++++++++++++++++++++++
security/landlock/domain.h | 68 ++++++++++++++++++++++++
security/landlock/ruleset.c | 6 +++
6 files changed, 270 insertions(+), 3 deletions(-)
Some minor questions below, but from an audit perspective this is okay.
Acked-by: Paul Moore <paul@paul-moore.com> (Audit)
You use __GFP_NOWARN in the other calls to audit_log_start(), did you
mean to use it here as well?
quoted hunk
+ if (!ab)
+ return;
+
+ WARN_ON_ONCE(node->id == 0);
+ audit_log_format(
+ ab,
+ "domain=%llx status=allocated mode=enforcing pid=%d uid=%u exe=",
+ node->id, pid_nr(node->details->pid),
+ from_kuid(&init_user_ns, node->details->cred->uid));
+ audit_log_untrustedstring(ab, node->details->exe_path);
+ audit_log_format(ab, " comm=");
+ audit_log_untrustedstring(ab, node->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(node->log_status, LANDLOCK_LOG_RECORDED);
+}
+
static struct landlock_hierarchy *
get_hierarchy(const struct landlock_ruleset *const domain, const size_t layer)
{
@@ -106,16 +142,24 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, if (!is_valid_request(request)) return;- if (!unlikely(audit_context() && audit_enabled))- return;- youngest_layer = request->layer_plus_one - 1; youngest_denied = get_hierarchy(subject->domain, youngest_layer);+ /*+ * Consistently keeps track of the number of denied access requests+ * even if audit is currently disabled, if audit rules currently+ * exclude this record type, or if landlock_restrict_self(2)'s flags+ * quiet logs.+ */+ atomic64_inc(&youngest_denied->num_denials);+ /* Ignores denials after an execution. */ if (!(subject->domain_exec & (1 << youngest_layer))) return;+ if (!unlikely(audit_context() && audit_enabled))+ return;+
Not a big deal either way, but it seems like the check above should
probably be in patch 09/24.
quoted hunk
ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN,
AUDIT_LANDLOCK_ACCESS);
if (!ab)
@@ -125,6 +169,46 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, log_blockers(ab, request->type); audit_log_lsm_data(ab, &request->audit); audit_log_end(ab);++ /* Logs this domain if it is the first time. */+ log_node(youngest_denied);+}++/**+ * landlock_log_drop_domain - Create an audit record when a domain is deleted+ *+ * @domain: The domain being deleted.+ *+ * 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.+ *+ * This record is not directly tied to a syscall entry.+ *+ * Called by the cred_free() hook, in an uninterruptible context.+ */+void landlock_log_drop_domain(const struct landlock_ruleset *const domain)+{+ struct audit_buffer *ab;++ if (WARN_ON_ONCE(!domain->hierarchy))+ return;++ if (!unlikely(audit_enabled))+ return;
I'm guessing you probably also want to check the audit context given
that you are doing it elsewhere?
+ /* Ignores domains that were not logged. */
+ if (READ_ONCE(domain->hierarchy->log_status) != LANDLOCK_LOG_RECORDED)
+ return;
+
+ ab = audit_log_start(audit_context(), GFP_ATOMIC,
+ AUDIT_LANDLOCK_DOMAIN);
+ if (!ab)
+ return;
+
+ audit_log_format(ab, "domain=%llx status=deallocated denials=%llu",
+ domain->hierarchy->id,
+ atomic64_read(&domain->hierarchy->num_denials));
+ audit_log_end(ab);
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
On Fri, Feb 14, 2025 at 05:52:49PM -0500, Paul Moore wrote:
On Jan 31, 2025 =?UTF-8?q?Micka=C3=ABl=20Sala=C3=BCn?= [off-list ref] wrote:
quoted
Asynchronously log domain information when it first denies an access.
This minimize the amount of generated logs, which makes it possible to
always log denials since they should not happen (except with the new
LANDLOCK_RESTRICT_SELF_QUIET flag). These records are identified with
the new AUDIT_LANDLOCK_DOMAIN type.
The AUDIT_LANDLOCK_DOMAIN message contains:
- the "domain" ID which is described;
- the "status" which can either be "allocated" or "deallocated";
- the "mode" which is for now only "enforcing";
- for the "allocated" status, a minimal set of properties to easily
identify the task that loaded the domain's policy with
landlock_restrict_self(2): "pid", "uid", executable path ("exe"), and
command line ("comm");
- for the "deallocated" state, the number of "denials" accounted to this
domain, which is at least 1.
This requires each domain to save these task properties at creation
time in the new struct landlock_details. A reference to the PID is kept
for the lifetime of the domain to avoid race conditions when
investigating the related task. The executable path is resolved and
stored to not keep a reference to the filesystem and block related
actions. All these metadata are stored for the lifetime of the related
domain and should then be minimal. The required memory is not accounted
to the task calling landlock_restrict_self(2) contrary to most other
Landlock allocations (see related comment).
The AUDIT_LANDLOCK_DOMAIN record follows the first AUDIT_LANDLOCK_ACCESS
record for the same domain, which is always followed by AUDIT_SYSCALL
and AUDIT_PROCTITLE. This is in line with the audit logic to first
record the cause of an event, and then add context with other types of
record.
Audit event sample for a first denial:
type=LANDLOCK_ACCESS msg=audit(1732186800.349:44): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd"
type=LANDLOCK_DOMAIN msg=audit(1732186800.349:44): domain=195ba459b status=allocated mode=enforcing pid=300 uid=0 exe="/root/sandboxer" comm="sandboxer"
type=SYSCALL msg=audit(1732186800.349:44): arch=c000003e syscall=101 success=no [...] pid=300 auid=0
Audit event sample for a following denial:
type=LANDLOCK_ACCESS msg=audit(1732186800.372:45): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd"
type=SYSCALL msg=audit(1732186800.372:45): arch=c000003e syscall=101 success=no [...] pid=300 auid=0
Log domain deletion with the "deallocated" state when a domain was
previously logged. This makes it possible for log parsers to free
potential resources when a domain ID will never show again.
The number of denied access requests is useful to easily check how many
access requests a domain blocked and potentially if some of them are
missing in logs because of audit rate limiting or audit rules. Rate
limiting could also drop this record though.
Audit event sample for a deletion of a domain that denied something:
type=LANDLOCK_DOMAIN msg=audit(1732186800.393:46): domain=195ba459b status=deallocated denials=2
Cc: Günther Noack <gnoack@google.com>
Cc: Paul Moore <paul@paul-moore.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-11-mic@digikod.net
---
Changes since v4:
- Rename AUDIT_LANDLOCK_DOM_{INFO,DROP} to AUDIT_LANDLOCK_DOMAIN and add
a "status" field, as requested by Paul.
- Add a harcoded "mode=enforcing" to leave room for a potential future
permissive mode, as suggested by Paul.
- Remove the "creation" timestamp, as suggested by Paul.
- Move LANDLOCK_PATH_MAX_SIZE to domain.h, check the size of the
greatest landlock_details at build time, and improve comments.
- Improve audit check in landlock_log_drop_domain().
- Add missing headers.
- Fix typo in comment.
- Rebase on top of the landlock_log_denial() and subject type changes.
Changes since v3:
- Log number of denied access requests with AUDIT_LANDLOCK_DOM_DROP
records, suggested by Tyler.
- Do not store a struct path pointer but the resolved string instead.
This enables us to not block unmount of the initially restricted task
executable's mount point. See the new get_current_info() and
get_current_exe(). A following patch add tests for this case.
- Create and allocate a new struct landlock_details for initially
restricted task's information.
- Remove audit_get_ctime() call, as requested by Paul. We now always
have a standalone timestamp per Landlock domain creations.
- Fix docstring.
Changes since v2:
- Fix docstring.
- Fix log_status check in log_hierarchy() to also log
LANDLOCK_LOG_DISABLED.
- Add audit's creation time to domain's properties.
- Use hexadecimal notation for domain IDs.
- Remove domain's parent records: parent domains are not really useful
in the logs. They will be available with the upcoming introspection
feature though.
- Extend commit message with audit's timestamp explanation.
Changes since v1:
- Add a ruleset's version for atomic logs.
- Rebased on the TCP patch series.
- Rename operation using "_" instead of "-".
- Rename AUDIT_LANDLOCK to AUDIT_LANDLOCK_RULESET.
- Only log when audit is enabled, but always set domain IDs.
- Don't log task's PID/TID with log_task() because it would be redundant
with the SYSCALL record.
- Remove race condition when logging ruleset creation and logging
ruleset modification while the related file descriptor was already
registered but the ruleset creation not logged yet.
- Fix domain drop logs.
- Move the domain drop record from the previous patch into this one.
- Do not log domain creation but log first domain use instead.
- Save task's properties that sandbox themselves.
---
include/uapi/linux/audit.h | 1 +
security/landlock/audit.c | 90 ++++++++++++++++++++++++++++++--
security/landlock/audit.h | 7 +++
security/landlock/domain.c | 101 ++++++++++++++++++++++++++++++++++++
security/landlock/domain.h | 68 ++++++++++++++++++++++++
security/landlock/ruleset.c | 6 +++
6 files changed, 270 insertions(+), 3 deletions(-)
Some minor questions below, but from an audit perspective this is okay.
Acked-by: Paul Moore <paul@paul-moore.com> (Audit)
You use __GFP_NOWARN in the other calls to audit_log_start(), did you
mean to use it here as well?
Good catch, this is at least inconsistent.
My initial though was that we should use __GFP_NOWARN for access
requests that can occure frequently, and common_lsm_audit() already uses
this flag, and the fact that audit events might not be logged at all for
other reasons (e.g. burst of entries). But __GFP_NOWARN is not use for
all audit_log_start() calls.
For domain allocations and deallocations, this should rarely happen
during the life of a process, and it looked reasonable to warn about an
issue at this point.
Are there guidance about __GFP_NOWARN for audit or other subsystems?
quoted
+ if (!ab)
+ return;
+
+ WARN_ON_ONCE(node->id == 0);
+ audit_log_format(
+ ab,
+ "domain=%llx status=allocated mode=enforcing pid=%d uid=%u exe=",
+ node->id, pid_nr(node->details->pid),
+ from_kuid(&init_user_ns, node->details->cred->uid));
+ audit_log_untrustedstring(ab, node->details->exe_path);
+ audit_log_format(ab, " comm=");
+ audit_log_untrustedstring(ab, node->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(node->log_status, LANDLOCK_LOG_RECORDED);
+}
+
static struct landlock_hierarchy *
get_hierarchy(const struct landlock_ruleset *const domain, const size_t layer)
{
quoted
@@ -106,16 +142,24 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, if (!is_valid_request(request)) return;- if (!unlikely(audit_context() && audit_enabled))- return;- youngest_layer = request->layer_plus_one - 1; youngest_denied = get_hierarchy(subject->domain, youngest_layer);+ /*+ * Consistently keeps track of the number of denied access requests+ * even if audit is currently disabled, if audit rules currently+ * exclude this record type, or if landlock_restrict_self(2)'s flags+ * quiet logs.+ */+ atomic64_inc(&youngest_denied->num_denials);+ /* Ignores denials after an execution. */ if (!(subject->domain_exec & (1 << youngest_layer))) return;+ if (!unlikely(audit_context() && audit_enabled))+ return;+
Not a big deal either way, but it seems like the check above should
probably be in patch 09/24.
It is in 09/24 but it's moved in this patch because we need to count
denials consistently, see a few lines above.
quoted
ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN,
AUDIT_LANDLOCK_ACCESS);
if (!ab)
@@ -125,6 +169,46 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, log_blockers(ab, request->type); audit_log_lsm_data(ab, &request->audit); audit_log_end(ab);++ /* Logs this domain if it is the first time. */+ log_node(youngest_denied);+}++/**+ * landlock_log_drop_domain - Create an audit record when a domain is deleted+ *+ * @domain: The domain being deleted.+ *+ * 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.+ *+ * This record is not directly tied to a syscall entry.+ *+ * Called by the cred_free() hook, in an uninterruptible context.+ */+void landlock_log_drop_domain(const struct landlock_ruleset *const domain)+{+ struct audit_buffer *ab;++ if (WARN_ON_ONCE(!domain->hierarchy))+ return;++ if (!unlikely(audit_enabled))+ return;
I'm guessing you probably also want to check the audit context given
that you are doing it elsewhere?
The context is NULL when this function is called, I guess because it is
not directly triggered by a syscall but deferred to the domain
deallocation.
quoted
+ /* Ignores domains that were not logged. */
+ if (READ_ONCE(domain->hierarchy->log_status) != LANDLOCK_LOG_RECORDED)
+ return;
+
+ ab = audit_log_start(audit_context(), GFP_ATOMIC,
+ AUDIT_LANDLOCK_DOMAIN);
+ if (!ab)
+ return;
+
+ audit_log_format(ab, "domain=%llx status=deallocated denials=%llu",
+ domain->hierarchy->id,
+ atomic64_read(&domain->hierarchy->num_denials));
+ audit_log_end(ab);
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
On Fri, Feb 14, 2025 at 05:52:47PM -0500, Paul Moore wrote:
On Jan 31, 2025 =?UTF-8?q?Micka=C3=ABl=20Sala=C3=BCn?= [off-list ref] wrote:
quoted
Add a new AUDIT_LANDLOCK_ACCESS record type dedicated to an access
request denied by a Landlock domain. AUDIT_LANDLOCK_ACCESS indicates
that something unexpected happened.
For now, only denied access are logged, which means that any
AUDIT_LANDLOCK_ACCESS record is always followed by a SYSCALL record with
"success=no". However, log parsers should check this syscall property
because this is the only sign that a request was denied. Indeed, we
could have "success=yes" if Landlock would support a "permissive" mode.
We could also add a new field for this mode to AUDIT_LANDLOCK_DOMAIN
(see following commit).
By default, the only logged access requests are those coming from the
same executed program that enforced the Landlock restriction on itself.
In other words, no audit record are created for a task after it called
execve(2). This is required to avoid log spam because programs may only
be aware of their own restrictions, but not the inherited ones.
Following commits will allow to conditionally generate
AUDIT_LANDLOCK_ACCESS records according to dedicated
landlock_restrict_self(2)'s flags.
The AUDIT_LANDLOCK_ACCESS message contains:
- the "domain" ID restricting the action on an object,
- the "blockers" that are missing to allow the requested access,
- a set of fields identifying the related object (e.g. task identified
with "opid" and "ocomm").
The blockers are implicit restrictions (e.g. ptrace), or explicit access
rights (e.g. filesystem), or explicit scopes (e.g. signal). This field
contains a list of at least one element, each separated with a comma.
The initial blocker is "ptrace", which describe all implicit Landlock
restrictions related to ptrace (e.g. deny tracing of tasks outside a
sandbox).
Add audit support to ptrace_access_check and ptrace_traceme hooks. For
the ptrace_access_check case, we log the current/parent domain and the
child task. For the ptrace_traceme case, we log the parent domain and
the parent task. Indeed, the requester is the current task, but the
action would be performed by the parent task.
Audit event sample:
type=LANDLOCK_ACCESS msg=audit(1729738800.349:44): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd"
type=SYSCALL msg=audit(1729738800.349:44): arch=c000003e syscall=101 success=no [...] pid=300 auid=0
A following commit adds user documentation.
Add KUnit tests to check reading of domain ID relative to layer level.
The quick return for non-landlocked tasks is moved from task_ptrace() to
each LSM hooks.
Because the landlock_log_denial() function is only called when an access
is denied, the compiler should be able to optimize the struct
landlock_request initializations. It is not useful to inline the
audit_enabled check because other computation are performed anyway, and
by the same landlock_log_denia() code.
Use scoped guards for RCU read-side critical sections.
Cc: Günther Noack <gnoack@google.com>
Cc: Paul Moore <paul@paul-moore.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20250131163059.1139617-10-mic@digikod.net
---
Changes since v4:
- Rename AUDIT_LANDLOCK_DENY to AUDIT_LANDLOCK_ACCESS, requested by
Paul.
- Make landlock_log_denial() get Landlock credential instead of Landlock
domain to be able to filter on the domain_exe variable.
- Rebase on top of the migration from struct landlock_ruleset to struct
landlock_cred_security.
- Rename landlock_init_current_hierarchy() to
landlock_init_hierarchy_log().
- Rebase on top of the scoped guard patches.
- By default, do not log denials after an execution.
- Use scoped guards for RCU read-side critical sections.
Changes since v3:
- Extend commit message.
Changes since v2:
- Log domain IDs as hexadecimal number: this is a more compact notation
(i.e. at least one less digit), it improves alignment in logs, and it
makes most IDs start with 1 as leading digit (because of the 2^32
minimal value). Do not use the "0x" prefix that would add useless
data to logs.
- Constify function arguments.
- Clean up Makefile entries.
Changes since v1:
- Move most audit code to this patch.
- Rebase on the TCP patch series.
- Don't log missing access right: simplify and make it generic for rule
types.
- Don't log errno and then don't wrap the error with
landlock_log_request(), as suggested by Jeff.
- Add a WARN_ON_ONCE() check to never dereference null pointers.
- Only log when audit is enabled.
- Don't log task's PID/TID with log_task() because it would be redundant
with the SYSCALL record.
- Move the "op" in front and rename "domain" to "denying_domain" to make
it more consistent with other entries.
- Don't update the request with the domain ID but add an helper to get
it from the layer masks (and in a following commit with a struct
file).
- Revamp get_domain_id_from_layer_masks() into
get_level_from_layer_masks().
- For ptrace_traceme, log the parent domain instead of the current one.
- Add documentation.
- Rename AUDIT_LANDLOCK_DENIAL to AUDIT_LANDLOCK_DENY.
- Only log the domain ID and the target task.
- Log "blockers", which are either implicit restrictions (e.g. ptrace)
or explicit access rights (e.g. filesystem), or scopes (e.g. signal).
- Don't log LSM hook names/operations.
- Pick an audit event ID folling the IPE ones.
- Add KUnit tests.
---
include/uapi/linux/audit.h | 3 +-
security/landlock/Makefile | 5 +-
security/landlock/audit.c | 146 ++++++++++++++++++++++++++++++++++++
security/landlock/audit.h | 53 +++++++++++++
security/landlock/domain.c | 28 +++++++
security/landlock/domain.h | 22 ++++++
security/landlock/ruleset.c | 6 ++
security/landlock/task.c | 96 ++++++++++++++++++------
8 files changed, 334 insertions(+), 25 deletions(-)
create mode 100644 security/landlock/audit.c
create mode 100644 security/landlock/audit.h
create mode 100644 security/landlock/domain.c
Based on previous discussions I'm under the impression that you are
planning to add a Landlock "permissive" mode at some point in the
future and based on the comments above you plan to add a "success="
field to the _ACCESS record defined here. There is no problem with
adding fields to an existing record, but the general guidance is that
new fields need to be added to the end of the record (limitations due
the the audit userspace and poor guidance in the early days of audit).
Assuming you are okay with that there is no need to change anything,
but if you would prefer the "permissive=" field to occur somewhere
else in the record you may want to consider adding a "permissive=no"
now. Otherwise this looks okay from an audit perspective.
[P.S. I just got to patch 10/24 and saw the enforcing field there,
the comments above still stand, but it looks like you chose to note
this in the _DOMAIN record, which is fine.]
The mode is indeed specified in the _DOMAIN record. I think the
syscall's success field should be enough for users in most cases, no
need to duplicate information.
Acked-by: Paul Moore <paul@paul-moore.com> (Audit)
--
paul-moore.com
On Fri, Jan 31, 2025 at 05:30:35PM +0100, Mickaël Salaün wrote:
Hi,
This patch series adds audit support to Landlock.
Logging denied requests is useful for different use cases:
- sysadmins: to look for users' issues,
- security experts: to detect attack attempts,
- power users: to understand denials,
- developers: to ease sandboxing support and get feedback from users.
Because of its unprivileged nature, Landlock can compose standalone
security policies (i.e. domains). To make logs useful, they need to
contain the most relevant Landlock domain that denied an action, and the
reason of such denial. This translates to the latest nested domain and
the related blockers: missing access rights or other kind of
restrictions.
# Changes from previous version
Remove the AUDIT_EXE_LANDLOCK_DENY audit rule and add 2 new
landlock_restrict_self(2) flags to filter Landlock audit events, which
makes 3 flags:
- LANDLOCK_RESTRICT_SELF_QUIET: do not log any denied access because of
this new domain.
- LANDLOCK_RESTRICT_SELF_QUIET_DESCENDENTS: do not log denied access
from child domains.
- LANDLOCK_RESTRICT_SELF_LOG_CROSS_EXEC: log denied access for processes
resulting from an execve(2), which is not the case by default anymore.
One patch was merged in mainline: 7ccbe076d987 ("lsm: Only build
lsm_audit.c if CONFIG_SECURITY and CONFIG_AUDIT are set").
# Design
Log records are created for any denied actions caused by a Landlock
policy, which means that a well-sandboxed applications should not log
anything except for unattended access requests that might be the result
of attacks or bugs.
However, sandbox tools creating restricted environments could lead to
abundant log entries because the sandboxed processes may not be aware of
the related restrictions. To avoid log spam, the
landlock_restrict_self(2) syscall gets a new
LANDLOCK_RESTRICT_SELF_QUIET flag to not log denials related to this
specific domain. Except for well-understood exceptions, this flag
should not be set. Indeed, applications sandboxing themselves should
only try to bypass their own sandbox if they are compromised, which
should ring a bell thanks to log events.
When an action is denied, the related Landlock domain ID is specified.
If this domain was not previously described in a log record, one is
created. This record contains the domain ID, its creation time, and
informations about the process that enforced the restriction (at the
time of the call to landlock_restrict_self): PID, UID, executable path,
and name (comm).
This new approach also brings building blocks for an upcoming
unprivileged introspection interface. The unique Landlock IDs will be
useful to tie audit log entries to running processes, and to get
properties of the related Landlock domains. This will replace the
previously logged ruleset properties.
What implications does this patch set have for Landlock's performance?
For some aspects of Landlock domains, when domains get merged, their
rules can potentially get merged into simpler "flattened"
representations at the cost of losing track about the original domain
for individual denials.
For instance, when a process enforces the following two rulesets
nested in each other:
* RS1 allowed to only connect to TCP ports {1, 2, 3}
* RS2 allowed to only connect to TCP ports {2, 3, 4}
Then the resulting merged domain could build the intersection of these
two sets {2, 3}, and store a smaller set of port numbers than the two
rulesets individually. Similar tricks would likely also be possible
for the rules for socket type restriction, as well as for
IOCTL-per-command allow-lists, if we had done that at that level of
granularity.
I realize that we are not doing this right now for ports, so it is
slightly speculative, but it would be an option in the future.
However, when we want to attribute each denial to the original domain
which caused it, that kind of optimization does not work any more.
In performance-sensitive environments that don't need Landlock
auditing, to what extent would users of such environments have to pay
a "hidden cost" of auditing because we can't do such "data structure
flattening" optimizations any more?
Do you have thoughts on how you want to strike the balance between
Landlock performance and logging accuracy?
–Günther
On Sat, Feb 22, 2025 at 08:47:40PM +0100, Günther Noack wrote:
On Fri, Jan 31, 2025 at 05:30:35PM +0100, Mickaël Salaün wrote:
quoted
Hi,
This patch series adds audit support to Landlock.
Logging denied requests is useful for different use cases:
- sysadmins: to look for users' issues,
- security experts: to detect attack attempts,
- power users: to understand denials,
- developers: to ease sandboxing support and get feedback from users.
Because of its unprivileged nature, Landlock can compose standalone
security policies (i.e. domains). To make logs useful, they need to
contain the most relevant Landlock domain that denied an action, and the
reason of such denial. This translates to the latest nested domain and
the related blockers: missing access rights or other kind of
restrictions.
# Changes from previous version
Remove the AUDIT_EXE_LANDLOCK_DENY audit rule and add 2 new
landlock_restrict_self(2) flags to filter Landlock audit events, which
makes 3 flags:
- LANDLOCK_RESTRICT_SELF_QUIET: do not log any denied access because of
this new domain.
- LANDLOCK_RESTRICT_SELF_QUIET_DESCENDENTS: do not log denied access
from child domains.
- LANDLOCK_RESTRICT_SELF_LOG_CROSS_EXEC: log denied access for processes
resulting from an execve(2), which is not the case by default anymore.
One patch was merged in mainline: 7ccbe076d987 ("lsm: Only build
lsm_audit.c if CONFIG_SECURITY and CONFIG_AUDIT are set").
# Design
Log records are created for any denied actions caused by a Landlock
policy, which means that a well-sandboxed applications should not log
anything except for unattended access requests that might be the result
of attacks or bugs.
However, sandbox tools creating restricted environments could lead to
abundant log entries because the sandboxed processes may not be aware of
the related restrictions. To avoid log spam, the
landlock_restrict_self(2) syscall gets a new
LANDLOCK_RESTRICT_SELF_QUIET flag to not log denials related to this
specific domain. Except for well-understood exceptions, this flag
should not be set. Indeed, applications sandboxing themselves should
only try to bypass their own sandbox if they are compromised, which
should ring a bell thanks to log events.
When an action is denied, the related Landlock domain ID is specified.
If this domain was not previously described in a log record, one is
created. This record contains the domain ID, its creation time, and
informations about the process that enforced the restriction (at the
time of the call to landlock_restrict_self): PID, UID, executable path,
and name (comm).
This new approach also brings building blocks for an upcoming
unprivileged introspection interface. The unique Landlock IDs will be
useful to tie audit log entries to running processes, and to get
properties of the related Landlock domains. This will replace the
previously logged ruleset properties.
What implications does this patch set have for Landlock's performance?
I did the benchmark for the hook_file_open() (with the changes explained
below) and there is no visible overhead for the worse case: opening /
takes ~8 micro seconds with and without sandboxing, and with and without
this patch series (when the request is allowed). I'll do the same for
other significant operations, including for denied requests, and include
the result in the next patch series.
For some aspects of Landlock domains, when domains get merged, their
rules can potentially get merged into simpler "flattened"
representations at the cost of losing track about the original domain
for individual denials.
For instance, when a process enforces the following two rulesets
nested in each other:
* RS1 allowed to only connect to TCP ports {1, 2, 3}
* RS2 allowed to only connect to TCP ports {2, 3, 4}
Then the resulting merged domain could build the intersection of these
two sets {2, 3}, and store a smaller set of port numbers than the two
rulesets individually. Similar tricks would likely also be possible
for the rules for socket type restriction, as well as for
IOCTL-per-command allow-lists, if we had done that at that level of
granularity.
I realize that we are not doing this right now for ports, so it is
slightly speculative, but it would be an option in the future.
However, when we want to attribute each denial to the original domain
which caused it, that kind of optimization does not work any more.
Yes, we are not doing this because of the requirement to identify
domains, see
http://lore.kernel.org/r/86db9124-ea11-0fa5-9dff-61744b2f80b4@digikod.net
We really need to be able to identify the cause/source of any denial.
This is a required feature for any decent access control system to
enable users to debug their systems/programs. I should probably extend
the Landlock guiding principles with this requirement.
This requirement might limit potential future improvements (at the
margin), but it's definitely worth it.
In performance-sensitive environments that don't need Landlock
auditing, to what extent would users of such environments have to pay
a "hidden cost" of auditing because we can't do such "data structure
flattening" optimizations any more?
In such environments, users can disable audit, and we could have a
dedicated type for this case, but I'm not convinced such optimization
and potential increased complexity would be worth it wrt other "slow"
paths. Anyway, we should first start optimizing by using a hash table.
Do you have thoughts on how you want to strike the balance between
Landlock performance and logging accuracy?
The initial versions of this audit support patch series were much more
verbose. With the current version I think we found the right balance
between the minimal useful information, verbosity, and complexity.
One important point to keep in mind is that the vast majority of this
new code is only executed for denied access requests. The approach is
for processes to only pay (a bit) when they request a denied access
(which should be rare).
However, I though the audit-specific variables (e.g. struct
landlock_request) initialization would be moveed near the
landlock_log_denial() calls (i.e. only initialized for denied
requests), but even with compiler optimizations, neither GCC nor clang
do that, so I'll do it manually in the next series.
For now, the two main potential slow paths for Landlock are the backward
path walks for filesystem access [1], and the use of red-black trees for
domains [2]. These slow paths are already difficult to see, so the
impact of audit support is negligible comparatively, especially for
legitimate use cases.
[1] https://github.com/landlock-lsm/linux/issues/9
[2] https://github.com/landlock-lsm/linux/issues/1
From: Paul Moore <paul@paul-moore.com> Date: 2025-02-26 23:41:48
On Tue, Feb 18, 2025 at 2:21 PM Mickaël Salaün [off-list ref] wrote:
Are there guidance about __GFP_NOWARN for audit or other subsystems?
Unfortunately I'm not aware of anything, and I too would be very
interested in learning if there was some solid guidance around the GFP
flags as the comment block in gfp_types.h is rather short.
--
paul-moore.com
On Fri, Jan 31, 2025 at 05:30:37PM +0100, Mickaël Salaün wrote:
quoted hunk
--- /dev/null+++ b/security/landlock/id.c
+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);
It feels like this should always need to succeed. Or to say it the
other way around: If this cmpxchg were to fail, the guarantees from
your commit message would be broken. Maybe it would be worth handling
that error case in a more direct way?
Maybe we can annotate this with an explanatory message,
to make it slightly clearer that this is the point of the test:
KUNIT_EXPECT_EQ_MSG(test, atomic64_read(&counter), first_init,
"should still have the same value after the subsequent init_id()");
–Günther
On Fri, Mar 07, 2025 at 03:15:44PM +0100, Günther Noack wrote:
On Fri, Jan 31, 2025 at 05:30:37PM +0100, Mickaël Salaün wrote:
quoted
--- /dev/null+++ b/security/landlock/id.c
+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);
It feels like this should always need to succeed. Or to say it the
other way around: If this cmpxchg were to fail, the guarantees from
your commit message would be broken. Maybe it would be worth handling
that error case in a more direct way?
This should always succeed and with the current code it always succeed
because there is only one call to this function. This
atomic64_cmpxchg() is a safeguard to be sure that, even if there are
several calls to this function, the counter will only be initialized
once (i.e. cmpxchg only sets the counter if its value was 0)
We could add a WARN_ON(atomic64_cmpxchg()) but I don't see the point.
Maybe we can annotate this with an explanatory message,
to make it slightly clearer that this is the point of the test:
KUNIT_EXPECT_EQ_MSG(test, atomic64_read(&counter), first_init,
"should still have the same value after the subsequent init_id()");