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