diff mbox series

[RFC,v2,10/14] landlock: Log file-related denials

Message ID 20241022161009.982584-11-mic@digikod.net (mailing list archive)
State Superseded
Delegated to: Paul Moore
Headers show
Series Landlock audit support | expand

Commit Message

Mickaël Salaün Oct. 22, 2024, 4:10 p.m. UTC
Add audit support for path_mkdir, path_mknod, path_symlink, path_unlink,
path_rmdir, path_truncate, path_link, path_rename, and file_open hooks.

Audit record sample for a link action:

  DENY:     domain=4533720568 blockers=fs_refer path="/usr/bin" dev="vda2" ino=351
  DOM_INFO: domain=4533720568 parent=0 pid=325 uid=0 exe="/root/sandboxer" comm="sandboxer"
  DENY:     domain=4533720568 blockers=fs_make_reg,fs_refer path="/usr/local" dev="vda2" ino=365
  SYSCALL:  arch=c000003e syscall=265 success=no exit=-13 ...

Cc: Günther Noack <gnoack@google.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20241022161009.982584-11-mic@digikod.net
---

Changes since v2:
* Revamp logging and support the path_link and path_rename hooks.
* Add KUnit tests.

Changes since v1:
* Move audit code to the ptrace patch.
---
 security/landlock/audit.c | 173 ++++++++++++++++++++++++++++++++++++--
 security/landlock/audit.h |   9 ++
 security/landlock/fs.c    |  64 +++++++++++---
 3 files changed, 229 insertions(+), 17 deletions(-)

Comments

Francis Laniel Oct. 25, 2024, 3:23 p.m. UTC | #1
Le mardi 22 octobre 2024, 18:10:05 CEST Mickaël Salaün a écrit :
> Add audit support for path_mkdir, path_mknod, path_symlink, path_unlink,
> path_rmdir, path_truncate, path_link, path_rename, and file_open hooks.
> 
> Audit record sample for a link action:
> 
>   DENY:     domain=4533720568 blockers=fs_refer path="/usr/bin" dev="vda2"
> ino=351 DOM_INFO: domain=4533720568 parent=0 pid=325 uid=0
> exe="/root/sandboxer" comm="sandboxer" DENY:     domain=4533720568
> blockers=fs_make_reg,fs_refer path="/usr/local" dev="vda2" ino=365 SYSCALL:
>  arch=c000003e syscall=265 success=no exit=-13 ...
> 
> Cc: Günther Noack <gnoack@google.com>
> Signed-off-by: Mickaël Salaün <mic@digikod.net>
> Link: https://lore.kernel.org/r/20241022161009.982584-11-mic@digikod.net
> ---
> 
> Changes since v2:
> * Revamp logging and support the path_link and path_rename hooks.
> * Add KUnit tests.
> 
> Changes since v1:
> * Move audit code to the ptrace patch.
> ---
>  security/landlock/audit.c | 173 ++++++++++++++++++++++++++++++++++++--
>  security/landlock/audit.h |   9 ++
>  security/landlock/fs.c    |  64 +++++++++++---
>  3 files changed, 229 insertions(+), 17 deletions(-)
> 
> diff --git a/security/landlock/audit.c b/security/landlock/audit.c
> index 4cd9407459d2..9c8b6c246884 100644
> --- a/security/landlock/audit.c
> +++ b/security/landlock/audit.c
> @@ -7,23 +7,55 @@
> 
>  #include <kunit/test.h>
>  #include <linux/audit.h>
> +#include <linux/bitops.h>
>  #include <linux/lsm_audit.h>
>  #include <linux/pid.h>
>  #include <linux/uidgid.h>
> +#include <uapi/linux/landlock.h>
> 
>  #include "audit.h"
> +#include "common.h"
>  #include "cred.h"
>  #include "domain.h"
>  #include "ruleset.h"
> 
> -static const char *get_blocker(const enum landlock_request_type type)
> +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 __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_LAYOUT:
> +		WARN_ON_ONCE(access_bit != -1);
>  		return "fs_change_layout";
> +
> +	case LANDLOCK_REQUEST_FS_ACCESS:
> +		if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(fs_access_strings)))
> +			return "unknown";
> +		return fs_access_strings[access_bit];
>  	}
> 
>  	WARN_ON_ONCE(1);
> @@ -31,9 +63,20 @@ static const char *get_blocker(const enum
> landlock_request_type type) }
> 
>  static void log_blockers(struct audit_buffer *const ab,
> -			 const enum landlock_request_type type)
> +			 const enum landlock_request_type type,
> +			 const access_mask_t access)
>  {
> -	audit_log_format(ab, "%s", get_blocker(type));
> +	const unsigned long access_mask = access;
> +	unsigned long access_bit;
> +	size_t i = 0;
> +
> +	for_each_set_bit(access_bit, &access_mask, BITS_PER_TYPE(access)) {
> +		audit_log_format(ab, "%s%s", (i == 0) ? "" : ",",
> +				 get_blocker(type, access_bit));
> +		i++;
> +	}
> +	if (i == 0)
> +		audit_log_format(ab, "%s", get_blocker(type, -1));
>  }
> 
>  static void log_node(struct landlock_hierarchy *const node)
> @@ -121,9 +164,110 @@ static void test_get_hierarchy(struct kunit *const
> test)
> 
>  #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;

Nit: should access_request being checked for not being NULL?

> +	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 bool is_valid_request(const struct landlock_request *const request)
>  {
> -	if (WARN_ON_ONCE(!request->layer_plus_one))
> +	if (WARN_ON_ONCE(!(!!request->layer_plus_one ^ !!request->access)))
> +		return false;
> +
> +	if (request->access) {
> +		if (WARN_ON_ONCE(!request->layer_masks))
> +			return false;
> +	} else {
> +		if (WARN_ON_ONCE(request->layer_masks))
> +			return false;
> +	}
> +
> +	if (WARN_ON_ONCE(!!request->layer_masks ^ !!request-
>layer_masks_size))
>  		return false;
> 
>  	return true;
> @@ -140,6 +284,7 @@ void landlock_log_denial(const struct landlock_ruleset
> *const domain, {
>  	struct audit_buffer *ab;
>  	struct landlock_hierarchy *youngest_denied;
> +	access_mask_t missing;
> 
>  	if (WARN_ON_ONCE(!domain || !domain->hierarchy || !request))
>  		return;
> @@ -155,9 +300,24 @@ void landlock_log_denial(const struct landlock_ruleset
> *const domain, if (!ab)
>  		return;
> 
> -	youngest_denied = get_hierarchy(domain, request->layer_plus_one - 1);
> +	missing = request->access;
> +	if (missing) {
> +		size_t youngest_layer;
> +
> +		/* Gets the nearest domain that denies the request. */
> +		if (request->layer_masks) {
> +			youngest_layer = get_denied_layer(
> +				domain, &missing, request->layer_masks,
> +				request->layer_masks_size);
> +		}
> +		youngest_denied = get_hierarchy(domain, youngest_layer);

If request->layer_masks is 0, it is possible to call get_hierarchy() with
uninitialized youngest_layer, is this wanted?

> +	} else {
> +		youngest_denied =
> +			get_hierarchy(domain, request->layer_plus_one - 1);
> +	}
> +
>  	audit_log_format(ab, "domain=%llu blockers=", youngest_denied->id);
> -	log_blockers(ab, request->type);
> +	log_blockers(ab, request->type, missing);
>  	audit_log_lsm_data(ab, &request->audit);
>  	audit_log_end(ab);
> 
> @@ -204,6 +364,7 @@ void landlock_log_drop_domain(const struct
> landlock_ruleset *const domain) static struct kunit_case test_cases[] = {
>  	/* clang-format off */
>  	KUNIT_CASE(test_get_hierarchy),
> +	KUNIT_CASE(test_get_denied_layer),
>  	{}
>  	/* clang-format on */
>  };
> diff --git a/security/landlock/audit.h b/security/landlock/audit.h
> index 6f5ad04b83c2..25fc8333cddc 100644
> --- a/security/landlock/audit.h
> +++ b/security/landlock/audit.h
> @@ -11,11 +11,13 @@
>  #include <linux/audit.h>
>  #include <linux/lsm_audit.h>
> 
> +#include "access.h"
>  #include "ruleset.h"
> 
>  enum landlock_request_type {
>  	LANDLOCK_REQUEST_PTRACE = 1,
>  	LANDLOCK_REQUEST_FS_CHANGE_LAYOUT,
> +	LANDLOCK_REQUEST_FS_ACCESS,
>  };
> 
>  /*
> @@ -33,6 +35,13 @@ struct landlock_request {
>  	 * extra one is useful to detect uninitialized field.
>  	 */
>  	size_t layer_plus_one;
> +
> +	/* Required field for configurable access control. */
> +	access_mask_t access;
> +
> +	/* Required fields for requests with layer masks. */
> +	const layer_mask_t (*layer_masks)[];
> +	size_t layer_masks_size;
>  };
> 
>  #ifdef CONFIG_AUDIT
> diff --git a/security/landlock/fs.c b/security/landlock/fs.c
> index a099167d2347..7f69bed9e095 100644
> --- a/security/landlock/fs.c
> +++ b/security/landlock/fs.c
> @@ -730,6 +730,7 @@ static void test_is_eacces_with_write(struct kunit
> *const test) *     those identified by @access_request_parent1).  This
> matrix can *     initially refer to domain layer masks and, when the
> accesses for the *     destination and source are the same, to requested
> layer masks. + * @log_request_parent1: Audit request to fill if the related
> access is denied. * @dentry_child1: Dentry to the initial child of the
> parent1 path.  This *     pointer must be NULL for non-refer actions (i.e.
> not link nor rename). * @access_request_parent2: Similar to
> @access_request_parent1 but for a @@ -738,6 +739,7 @@ static void
> test_is_eacces_with_write(struct kunit *const test) *     the source.  Must
> be set to 0 when using a simple path request. * @layer_masks_parent2:
> Similar to @layer_masks_parent1 but for a refer *     action.  This must be
> NULL otherwise.
> + * @log_request_parent2: Audit request to fill if the related access is
> denied. * @dentry_child2: Dentry to the initial child of the parent2 path. 
> This *     pointer is only set for RENAME_EXCHANGE actions and must be NULL
> *     otherwise.
> @@ -757,10 +759,12 @@ static bool is_access_to_paths_allowed(
>  	const struct path *const path,
>  	const access_mask_t access_request_parent1,
>  	layer_mask_t (*const layer_masks_parent1)[LANDLOCK_NUM_ACCESS_FS],
> -	const struct dentry *const dentry_child1,
> +	struct landlock_request *const log_request_parent1,
> +	struct dentry *const dentry_child1,
>  	const access_mask_t access_request_parent2,
>  	layer_mask_t (*const layer_masks_parent2)[LANDLOCK_NUM_ACCESS_FS],
> -	const struct dentry *const dentry_child2)
> +	struct landlock_request *const log_request_parent2,
> +	struct dentry *const dentry_child2)
>  {
>  	bool allowed_parent1 = false, allowed_parent2 = false, is_dom_check,
>  	     child1_is_directory = true, child2_is_directory = true;
> @@ -907,6 +911,24 @@ static bool is_access_to_paths_allowed(
>  	}
>  	path_put(&walker_path);
> 
> +	if (!allowed_parent1 && log_request_parent1) {
> +		log_request_parent1->type = LANDLOCK_REQUEST_FS_ACCESS,
> +		log_request_parent1->audit.type = LSM_AUDIT_DATA_PATH,
> +		log_request_parent1->audit.u.path = *path;
> +		log_request_parent1->access = access_request_parent1;
> +		log_request_parent1->layer_masks = layer_masks_parent1;
> +		log_request_parent1->layer_masks_size =
> +			ARRAY_SIZE(*layer_masks_parent1);
> +	}
> +	if (!allowed_parent2 && log_request_parent2) {
> +		log_request_parent2->type = LANDLOCK_REQUEST_FS_ACCESS,
> +		log_request_parent2->audit.type = LSM_AUDIT_DATA_PATH,
> +		log_request_parent2->audit.u.path = *path;
> +		log_request_parent2->access = access_request_parent2;
> +		log_request_parent2->layer_masks = layer_masks_parent2;
> +		log_request_parent2->layer_masks_size =
> +			ARRAY_SIZE(*layer_masks_parent2);
> +	}

You may want to add a function to set these fields in log_request.

>  	return allowed_parent1 && allowed_parent2;
>  }
> 
> @@ -915,6 +937,7 @@ static int current_check_access_path(const struct path
> *const path, {
>  	const struct landlock_ruleset *const dom = get_current_fs_domain();
>  	layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
> +	struct landlock_request request = {};
> 
>  	if (!dom)
>  		return 0;
> @@ -922,9 +945,10 @@ static int current_check_access_path(const struct path
> *const path, access_request = landlock_init_layer_masks(
>  		dom, access_request, &layer_masks, LANDLOCK_KEY_INODE);
>  	if (is_access_to_paths_allowed(dom, path, access_request, 
&layer_masks,
> -				       NULL, 0, NULL, NULL))
> +				       &request, NULL, 0, NULL, NULL, NULL))
>  		return 0;
> 
> +	landlock_log_denial(dom, &request);
>  	return -EACCES;
>  }
> 
> @@ -1093,6 +1117,7 @@ static int current_check_refer_path(struct dentry
> *const old_dentry, struct dentry *old_parent;
>  	layer_mask_t layer_masks_parent1[LANDLOCK_NUM_ACCESS_FS] = {},
>  		     layer_masks_parent2[LANDLOCK_NUM_ACCESS_FS] = {};
> +	struct landlock_request request1 = {}, request2 = {};
> 
>  	if (!dom)
>  		return 0;
> @@ -1124,10 +1149,13 @@ static int current_check_refer_path(struct dentry
> *const old_dentry, access_request_parent1 = landlock_init_layer_masks(
>  			dom, access_request_parent1 | access_request_parent2,
>  			&layer_masks_parent1, LANDLOCK_KEY_INODE);
> -		if (is_access_to_paths_allowed(
> -			    dom, new_dir, access_request_parent1,
> -			    &layer_masks_parent1, NULL, 0, NULL, NULL))
> +		if (is_access_to_paths_allowed(dom, new_dir,
> +					       access_request_parent1,
> +					       &layer_masks_parent1, &request1,
> +					       NULL, 0, NULL, NULL, NULL))
>  			return 0;
> +
> +		landlock_log_denial(dom, &request1);
>  		return -EACCES;
>  	}
> 
> @@ -1162,12 +1190,22 @@ static int current_check_refer_path(struct dentry
> *const old_dentry, * parent access rights.  This will be useful to compare
> with the * destination parent access rights.
>  	 */
> -	if (is_access_to_paths_allowed(
> -		    dom, &mnt_dir, access_request_parent1, 
&layer_masks_parent1,
> -		    old_dentry, access_request_parent2, &layer_masks_parent2,
> -		    exchange ? new_dentry : NULL))
> +	if (is_access_to_paths_allowed(dom, &mnt_dir, access_request_parent1,
> +				       &layer_masks_parent1, &request1,
> +				       old_dentry, access_request_parent2,
> +				       &layer_masks_parent2, &request2,
> +				       exchange ? new_dentry : NULL))
>  		return 0;
> 
> +	if (request1.access) {
> +		request1.audit.u.path.dentry = old_parent;
> +		landlock_log_denial(dom, &request1);
> +	}
> +	if (request2.access) {
> +		request2.audit.u.path.dentry = new_dir->dentry;
> +		landlock_log_denial(dom, &request2);
> +	}
> +
>  	/*
>  	 * This prioritizes EACCES over EXDEV for all actions, including
>  	 * renames with RENAME_EXCHANGE.
> @@ -1546,6 +1584,7 @@ static int hook_file_open(struct file *const file)
>  		optional_access;
>  	const struct landlock_ruleset *const dom = landlock_match_ruleset(
>  		landlock_cred(file->f_cred)->domain, any_fs);
> +	struct landlock_request request = {};
> 
>  	if (!dom)
>  		return 0;
> @@ -1571,7 +1610,7 @@ static int hook_file_open(struct file *const file)
>  		    dom, &file->f_path,
>  		    landlock_init_layer_masks(dom, 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 {
>  		unsigned long access_bit;
> @@ -1601,6 +1640,9 @@ static int hook_file_open(struct file *const file)
>  	if ((open_access_request & allowed_access) == open_access_request)
>  		return 0;
> 
> +	/* Sets access to reflect the actual request. */
> +	request.access = open_access_request;
> +	landlock_log_denial(dom, &request);
>  	return -EACCES;
>  }
Mickaël Salaün Nov. 13, 2024, 3:21 p.m. UTC | #2
On Fri, Oct 25, 2024 at 05:23:48PM +0200, Francis Laniel wrote:
> Le mardi 22 octobre 2024, 18:10:05 CEST Mickaël Salaün a écrit :
> > Add audit support for path_mkdir, path_mknod, path_symlink, path_unlink,
> > path_rmdir, path_truncate, path_link, path_rename, and file_open hooks.
> > 
> > Audit record sample for a link action:
> > 
> >   DENY:     domain=4533720568 blockers=fs_refer path="/usr/bin" dev="vda2"
> > ino=351 DOM_INFO: domain=4533720568 parent=0 pid=325 uid=0
> > exe="/root/sandboxer" comm="sandboxer" DENY:     domain=4533720568
> > blockers=fs_make_reg,fs_refer path="/usr/local" dev="vda2" ino=365 SYSCALL:
> >  arch=c000003e syscall=265 success=no exit=-13 ...
> > 
> > Cc: Günther Noack <gnoack@google.com>
> > Signed-off-by: Mickaël Salaün <mic@digikod.net>
> > Link: https://lore.kernel.org/r/20241022161009.982584-11-mic@digikod.net
> > ---
> > 
> > Changes since v2:
> > * Revamp logging and support the path_link and path_rename hooks.
> > * Add KUnit tests.
> > 
> > Changes since v1:
> > * Move audit code to the ptrace patch.
> > ---
> >  security/landlock/audit.c | 173 ++++++++++++++++++++++++++++++++++++--
> >  security/landlock/audit.h |   9 ++
> >  security/landlock/fs.c    |  64 +++++++++++---
> >  3 files changed, 229 insertions(+), 17 deletions(-)
> > 
> > diff --git a/security/landlock/audit.c b/security/landlock/audit.c
> > index 4cd9407459d2..9c8b6c246884 100644
> > --- a/security/landlock/audit.c
> > +++ b/security/landlock/audit.c
> > @@ -7,23 +7,55 @@
> > 
> >  #include <kunit/test.h>
> >  #include <linux/audit.h>
> > +#include <linux/bitops.h>
> >  #include <linux/lsm_audit.h>
> >  #include <linux/pid.h>
> >  #include <linux/uidgid.h>
> > +#include <uapi/linux/landlock.h>
> > 
> >  #include "audit.h"
> > +#include "common.h"
> >  #include "cred.h"
> >  #include "domain.h"
> >  #include "ruleset.h"
> > 
> > -static const char *get_blocker(const enum landlock_request_type type)
> > +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 __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_LAYOUT:
> > +		WARN_ON_ONCE(access_bit != -1);
> >  		return "fs_change_layout";
> > +
> > +	case LANDLOCK_REQUEST_FS_ACCESS:
> > +		if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(fs_access_strings)))
> > +			return "unknown";
> > +		return fs_access_strings[access_bit];
> >  	}
> > 
> >  	WARN_ON_ONCE(1);
> > @@ -31,9 +63,20 @@ static const char *get_blocker(const enum
> > landlock_request_type type) }
> > 
> >  static void log_blockers(struct audit_buffer *const ab,
> > -			 const enum landlock_request_type type)
> > +			 const enum landlock_request_type type,
> > +			 const access_mask_t access)
> >  {
> > -	audit_log_format(ab, "%s", get_blocker(type));
> > +	const unsigned long access_mask = access;
> > +	unsigned long access_bit;
> > +	size_t i = 0;
> > +
> > +	for_each_set_bit(access_bit, &access_mask, BITS_PER_TYPE(access)) {
> > +		audit_log_format(ab, "%s%s", (i == 0) ? "" : ",",
> > +				 get_blocker(type, access_bit));
> > +		i++;
> > +	}
> > +	if (i == 0)
> > +		audit_log_format(ab, "%s", get_blocker(type, -1));
> >  }
> > 
> >  static void log_node(struct landlock_hierarchy *const node)
> > @@ -121,9 +164,110 @@ static void test_get_hierarchy(struct kunit *const
> > test)
> > 
> >  #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;
> 
> Nit: should access_request being checked for not being NULL?

This is not necessary because this helper is private and the pointer is
always refering to the stack.

> 
> > +	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 bool is_valid_request(const struct landlock_request *const request)
> >  {
> > -	if (WARN_ON_ONCE(!request->layer_plus_one))
> > +	if (WARN_ON_ONCE(!(!!request->layer_plus_one ^ !!request->access)))
> > +		return false;
> > +
> > +	if (request->access) {
> > +		if (WARN_ON_ONCE(!request->layer_masks))
> > +			return false;
> > +	} else {
> > +		if (WARN_ON_ONCE(request->layer_masks))
> > +			return false;
> > +	}
> > +
> > +	if (WARN_ON_ONCE(!!request->layer_masks ^ !!request-
> >layer_masks_size))
> >  		return false;
> > 
> >  	return true;
> > @@ -140,6 +284,7 @@ void landlock_log_denial(const struct landlock_ruleset
> > *const domain, {
> >  	struct audit_buffer *ab;
> >  	struct landlock_hierarchy *youngest_denied;
> > +	access_mask_t missing;
> > 
> >  	if (WARN_ON_ONCE(!domain || !domain->hierarchy || !request))
> >  		return;
> > @@ -155,9 +300,24 @@ void landlock_log_denial(const struct landlock_ruleset
> > *const domain, if (!ab)
> >  		return;
> > 
> > -	youngest_denied = get_hierarchy(domain, request->layer_plus_one - 1);
> > +	missing = request->access;
> > +	if (missing) {
> > +		size_t youngest_layer;
> > +
> > +		/* Gets the nearest domain that denies the request. */
> > +		if (request->layer_masks) {
> > +			youngest_layer = get_denied_layer(
> > +				domain, &missing, request->layer_masks,
> > +				request->layer_masks_size);
> > +		}
> > +		youngest_denied = get_hierarchy(domain, youngest_layer);
> 
> If request->layer_masks is 0, it is possible to call get_hierarchy() with
> uninitialized youngest_layer, is this wanted?

Well spotted. This patch seems indeed buggy because I created several
patches touching the same function, but the final code (with all the
patches applied) always initializes youngest_denied.  The current calls
to landlock_log_denial() also always set request->layer_mask, but I'll
fix this patch to avoid confusion.

> 
> > +	} else {
> > +		youngest_denied =
> > +			get_hierarchy(domain, request->layer_plus_one - 1);
> > +	}
> > +
> >  	audit_log_format(ab, "domain=%llu blockers=", youngest_denied->id);
> > -	log_blockers(ab, request->type);
> > +	log_blockers(ab, request->type, missing);
> >  	audit_log_lsm_data(ab, &request->audit);
> >  	audit_log_end(ab);
> > 
> > @@ -204,6 +364,7 @@ void landlock_log_drop_domain(const struct
> > landlock_ruleset *const domain) static struct kunit_case test_cases[] = {
> >  	/* clang-format off */
> >  	KUNIT_CASE(test_get_hierarchy),
> > +	KUNIT_CASE(test_get_denied_layer),
> >  	{}
> >  	/* clang-format on */
> >  };
> > diff --git a/security/landlock/audit.h b/security/landlock/audit.h
> > index 6f5ad04b83c2..25fc8333cddc 100644
> > --- a/security/landlock/audit.h
> > +++ b/security/landlock/audit.h
> > @@ -11,11 +11,13 @@
> >  #include <linux/audit.h>
> >  #include <linux/lsm_audit.h>
> > 
> > +#include "access.h"
> >  #include "ruleset.h"
> > 
> >  enum landlock_request_type {
> >  	LANDLOCK_REQUEST_PTRACE = 1,
> >  	LANDLOCK_REQUEST_FS_CHANGE_LAYOUT,
> > +	LANDLOCK_REQUEST_FS_ACCESS,
> >  };
> > 
> >  /*
> > @@ -33,6 +35,13 @@ struct landlock_request {
> >  	 * extra one is useful to detect uninitialized field.
> >  	 */
> >  	size_t layer_plus_one;
> > +
> > +	/* Required field for configurable access control. */
> > +	access_mask_t access;
> > +
> > +	/* Required fields for requests with layer masks. */
> > +	const layer_mask_t (*layer_masks)[];
> > +	size_t layer_masks_size;
> >  };
> > 
> >  #ifdef CONFIG_AUDIT
> > diff --git a/security/landlock/fs.c b/security/landlock/fs.c
> > index a099167d2347..7f69bed9e095 100644
> > --- a/security/landlock/fs.c
> > +++ b/security/landlock/fs.c
> > @@ -730,6 +730,7 @@ static void test_is_eacces_with_write(struct kunit
> > *const test) *     those identified by @access_request_parent1).  This
> > matrix can *     initially refer to domain layer masks and, when the
> > accesses for the *     destination and source are the same, to requested
> > layer masks. + * @log_request_parent1: Audit request to fill if the related
> > access is denied. * @dentry_child1: Dentry to the initial child of the
> > parent1 path.  This *     pointer must be NULL for non-refer actions (i.e.
> > not link nor rename). * @access_request_parent2: Similar to
> > @access_request_parent1 but for a @@ -738,6 +739,7 @@ static void
> > test_is_eacces_with_write(struct kunit *const test) *     the source.  Must
> > be set to 0 when using a simple path request. * @layer_masks_parent2:
> > Similar to @layer_masks_parent1 but for a refer *     action.  This must be
> > NULL otherwise.
> > + * @log_request_parent2: Audit request to fill if the related access is
> > denied. * @dentry_child2: Dentry to the initial child of the parent2 path. 
> > This *     pointer is only set for RENAME_EXCHANGE actions and must be NULL
> > *     otherwise.
> > @@ -757,10 +759,12 @@ static bool is_access_to_paths_allowed(
> >  	const struct path *const path,
> >  	const access_mask_t access_request_parent1,
> >  	layer_mask_t (*const layer_masks_parent1)[LANDLOCK_NUM_ACCESS_FS],
> > -	const struct dentry *const dentry_child1,
> > +	struct landlock_request *const log_request_parent1,
> > +	struct dentry *const dentry_child1,
> >  	const access_mask_t access_request_parent2,
> >  	layer_mask_t (*const layer_masks_parent2)[LANDLOCK_NUM_ACCESS_FS],
> > -	const struct dentry *const dentry_child2)
> > +	struct landlock_request *const log_request_parent2,
> > +	struct dentry *const dentry_child2)
> >  {
> >  	bool allowed_parent1 = false, allowed_parent2 = false, is_dom_check,
> >  	     child1_is_directory = true, child2_is_directory = true;
> > @@ -907,6 +911,24 @@ static bool is_access_to_paths_allowed(
> >  	}
> >  	path_put(&walker_path);
> > 
> > +	if (!allowed_parent1 && log_request_parent1) {
> > +		log_request_parent1->type = LANDLOCK_REQUEST_FS_ACCESS,
> > +		log_request_parent1->audit.type = LSM_AUDIT_DATA_PATH,
> > +		log_request_parent1->audit.u.path = *path;
> > +		log_request_parent1->access = access_request_parent1;
> > +		log_request_parent1->layer_masks = layer_masks_parent1;
> > +		log_request_parent1->layer_masks_size =
> > +			ARRAY_SIZE(*layer_masks_parent1);
> > +	}
> > +	if (!allowed_parent2 && log_request_parent2) {
> > +		log_request_parent2->type = LANDLOCK_REQUEST_FS_ACCESS,
> > +		log_request_parent2->audit.type = LSM_AUDIT_DATA_PATH,
> > +		log_request_parent2->audit.u.path = *path;
> > +		log_request_parent2->access = access_request_parent2;
> > +		log_request_parent2->layer_masks = layer_masks_parent2;
> > +		log_request_parent2->layer_masks_size =
> > +			ARRAY_SIZE(*layer_masks_parent2);
> > +	}
> 
> You may want to add a function to set these fields in log_request.

There would only be two calls to this function and with at least four
arguments, so for simplicity, I don't think it's worth it.
diff mbox series

Patch

diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index 4cd9407459d2..9c8b6c246884 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -7,23 +7,55 @@ 
 
 #include <kunit/test.h>
 #include <linux/audit.h>
+#include <linux/bitops.h>
 #include <linux/lsm_audit.h>
 #include <linux/pid.h>
 #include <linux/uidgid.h>
+#include <uapi/linux/landlock.h>
 
 #include "audit.h"
+#include "common.h"
 #include "cred.h"
 #include "domain.h"
 #include "ruleset.h"
 
-static const char *get_blocker(const enum landlock_request_type type)
+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 __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_LAYOUT:
+		WARN_ON_ONCE(access_bit != -1);
 		return "fs_change_layout";
+
+	case LANDLOCK_REQUEST_FS_ACCESS:
+		if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(fs_access_strings)))
+			return "unknown";
+		return fs_access_strings[access_bit];
 	}
 
 	WARN_ON_ONCE(1);
@@ -31,9 +63,20 @@  static const char *get_blocker(const enum landlock_request_type type)
 }
 
 static void log_blockers(struct audit_buffer *const ab,
-			 const enum landlock_request_type type)
+			 const enum landlock_request_type type,
+			 const access_mask_t access)
 {
-	audit_log_format(ab, "%s", get_blocker(type));
+	const unsigned long access_mask = access;
+	unsigned long access_bit;
+	size_t i = 0;
+
+	for_each_set_bit(access_bit, &access_mask, BITS_PER_TYPE(access)) {
+		audit_log_format(ab, "%s%s", (i == 0) ? "" : ",",
+				 get_blocker(type, access_bit));
+		i++;
+	}
+	if (i == 0)
+		audit_log_format(ab, "%s", get_blocker(type, -1));
 }
 
 static void log_node(struct landlock_hierarchy *const node)
@@ -121,9 +164,110 @@  static void test_get_hierarchy(struct kunit *const test)
 
 #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 bool is_valid_request(const struct landlock_request *const request)
 {
-	if (WARN_ON_ONCE(!request->layer_plus_one))
+	if (WARN_ON_ONCE(!(!!request->layer_plus_one ^ !!request->access)))
+		return false;
+
+	if (request->access) {
+		if (WARN_ON_ONCE(!request->layer_masks))
+			return false;
+	} else {
+		if (WARN_ON_ONCE(request->layer_masks))
+			return false;
+	}
+
+	if (WARN_ON_ONCE(!!request->layer_masks ^ !!request->layer_masks_size))
 		return false;
 
 	return true;
@@ -140,6 +284,7 @@  void landlock_log_denial(const struct landlock_ruleset *const domain,
 {
 	struct audit_buffer *ab;
 	struct landlock_hierarchy *youngest_denied;
+	access_mask_t missing;
 
 	if (WARN_ON_ONCE(!domain || !domain->hierarchy || !request))
 		return;
@@ -155,9 +300,24 @@  void landlock_log_denial(const struct landlock_ruleset *const domain,
 	if (!ab)
 		return;
 
-	youngest_denied = get_hierarchy(domain, request->layer_plus_one - 1);
+	missing = request->access;
+	if (missing) {
+		size_t youngest_layer;
+
+		/* Gets the nearest domain that denies the request. */
+		if (request->layer_masks) {
+			youngest_layer = get_denied_layer(
+				domain, &missing, request->layer_masks,
+				request->layer_masks_size);
+		}
+		youngest_denied = get_hierarchy(domain, youngest_layer);
+	} else {
+		youngest_denied =
+			get_hierarchy(domain, request->layer_plus_one - 1);
+	}
+
 	audit_log_format(ab, "domain=%llu blockers=", youngest_denied->id);
-	log_blockers(ab, request->type);
+	log_blockers(ab, request->type, missing);
 	audit_log_lsm_data(ab, &request->audit);
 	audit_log_end(ab);
 
@@ -204,6 +364,7 @@  void landlock_log_drop_domain(const struct landlock_ruleset *const domain)
 static struct kunit_case test_cases[] = {
 	/* clang-format off */
 	KUNIT_CASE(test_get_hierarchy),
+	KUNIT_CASE(test_get_denied_layer),
 	{}
 	/* clang-format on */
 };
diff --git a/security/landlock/audit.h b/security/landlock/audit.h
index 6f5ad04b83c2..25fc8333cddc 100644
--- a/security/landlock/audit.h
+++ b/security/landlock/audit.h
@@ -11,11 +11,13 @@ 
 #include <linux/audit.h>
 #include <linux/lsm_audit.h>
 
+#include "access.h"
 #include "ruleset.h"
 
 enum landlock_request_type {
 	LANDLOCK_REQUEST_PTRACE = 1,
 	LANDLOCK_REQUEST_FS_CHANGE_LAYOUT,
+	LANDLOCK_REQUEST_FS_ACCESS,
 };
 
 /*
@@ -33,6 +35,13 @@  struct landlock_request {
 	 * extra one is useful to detect uninitialized field.
 	 */
 	size_t layer_plus_one;
+
+	/* Required field for configurable access control. */
+	access_mask_t access;
+
+	/* Required fields for requests with layer masks. */
+	const layer_mask_t (*layer_masks)[];
+	size_t layer_masks_size;
 };
 
 #ifdef CONFIG_AUDIT
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index a099167d2347..7f69bed9e095 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -730,6 +730,7 @@  static void test_is_eacces_with_write(struct kunit *const test)
  *     those identified by @access_request_parent1).  This matrix can
  *     initially refer to domain layer masks and, when the accesses for the
  *     destination and source are the same, to requested layer masks.
+ * @log_request_parent1: Audit request to fill if the related access is denied.
  * @dentry_child1: Dentry to the initial child of the parent1 path.  This
  *     pointer must be NULL for non-refer actions (i.e. not link nor rename).
  * @access_request_parent2: Similar to @access_request_parent1 but for a
@@ -738,6 +739,7 @@  static void test_is_eacces_with_write(struct kunit *const test)
  *     the source.  Must be set to 0 when using a simple path request.
  * @layer_masks_parent2: Similar to @layer_masks_parent1 but for a refer
  *     action.  This must be NULL otherwise.
+ * @log_request_parent2: Audit request to fill if the related access is denied.
  * @dentry_child2: Dentry to the initial child of the parent2 path.  This
  *     pointer is only set for RENAME_EXCHANGE actions and must be NULL
  *     otherwise.
@@ -757,10 +759,12 @@  static bool is_access_to_paths_allowed(
 	const struct path *const path,
 	const access_mask_t access_request_parent1,
 	layer_mask_t (*const layer_masks_parent1)[LANDLOCK_NUM_ACCESS_FS],
-	const struct dentry *const dentry_child1,
+	struct landlock_request *const log_request_parent1,
+	struct dentry *const dentry_child1,
 	const access_mask_t access_request_parent2,
 	layer_mask_t (*const layer_masks_parent2)[LANDLOCK_NUM_ACCESS_FS],
-	const struct dentry *const dentry_child2)
+	struct landlock_request *const log_request_parent2,
+	struct dentry *const dentry_child2)
 {
 	bool allowed_parent1 = false, allowed_parent2 = false, is_dom_check,
 	     child1_is_directory = true, child2_is_directory = true;
@@ -907,6 +911,24 @@  static bool is_access_to_paths_allowed(
 	}
 	path_put(&walker_path);
 
+	if (!allowed_parent1 && log_request_parent1) {
+		log_request_parent1->type = LANDLOCK_REQUEST_FS_ACCESS,
+		log_request_parent1->audit.type = LSM_AUDIT_DATA_PATH,
+		log_request_parent1->audit.u.path = *path;
+		log_request_parent1->access = access_request_parent1;
+		log_request_parent1->layer_masks = layer_masks_parent1;
+		log_request_parent1->layer_masks_size =
+			ARRAY_SIZE(*layer_masks_parent1);
+	}
+	if (!allowed_parent2 && log_request_parent2) {
+		log_request_parent2->type = LANDLOCK_REQUEST_FS_ACCESS,
+		log_request_parent2->audit.type = LSM_AUDIT_DATA_PATH,
+		log_request_parent2->audit.u.path = *path;
+		log_request_parent2->access = access_request_parent2;
+		log_request_parent2->layer_masks = layer_masks_parent2;
+		log_request_parent2->layer_masks_size =
+			ARRAY_SIZE(*layer_masks_parent2);
+	}
 	return allowed_parent1 && allowed_parent2;
 }
 
@@ -915,6 +937,7 @@  static int current_check_access_path(const struct path *const path,
 {
 	const struct landlock_ruleset *const dom = get_current_fs_domain();
 	layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
+	struct landlock_request request = {};
 
 	if (!dom)
 		return 0;
@@ -922,9 +945,10 @@  static int current_check_access_path(const struct path *const path,
 	access_request = landlock_init_layer_masks(
 		dom, access_request, &layer_masks, LANDLOCK_KEY_INODE);
 	if (is_access_to_paths_allowed(dom, path, access_request, &layer_masks,
-				       NULL, 0, NULL, NULL))
+				       &request, NULL, 0, NULL, NULL, NULL))
 		return 0;
 
+	landlock_log_denial(dom, &request);
 	return -EACCES;
 }
 
@@ -1093,6 +1117,7 @@  static int current_check_refer_path(struct dentry *const old_dentry,
 	struct dentry *old_parent;
 	layer_mask_t layer_masks_parent1[LANDLOCK_NUM_ACCESS_FS] = {},
 		     layer_masks_parent2[LANDLOCK_NUM_ACCESS_FS] = {};
+	struct landlock_request request1 = {}, request2 = {};
 
 	if (!dom)
 		return 0;
@@ -1124,10 +1149,13 @@  static int current_check_refer_path(struct dentry *const old_dentry,
 		access_request_parent1 = landlock_init_layer_masks(
 			dom, access_request_parent1 | access_request_parent2,
 			&layer_masks_parent1, LANDLOCK_KEY_INODE);
-		if (is_access_to_paths_allowed(
-			    dom, new_dir, access_request_parent1,
-			    &layer_masks_parent1, NULL, 0, NULL, NULL))
+		if (is_access_to_paths_allowed(dom, new_dir,
+					       access_request_parent1,
+					       &layer_masks_parent1, &request1,
+					       NULL, 0, NULL, NULL, NULL))
 			return 0;
+
+		landlock_log_denial(dom, &request1);
 		return -EACCES;
 	}
 
@@ -1162,12 +1190,22 @@  static int current_check_refer_path(struct dentry *const old_dentry,
 	 * parent access rights.  This will be useful to compare with the
 	 * destination parent access rights.
 	 */
-	if (is_access_to_paths_allowed(
-		    dom, &mnt_dir, access_request_parent1, &layer_masks_parent1,
-		    old_dentry, access_request_parent2, &layer_masks_parent2,
-		    exchange ? new_dentry : NULL))
+	if (is_access_to_paths_allowed(dom, &mnt_dir, access_request_parent1,
+				       &layer_masks_parent1, &request1,
+				       old_dentry, access_request_parent2,
+				       &layer_masks_parent2, &request2,
+				       exchange ? new_dentry : NULL))
 		return 0;
 
+	if (request1.access) {
+		request1.audit.u.path.dentry = old_parent;
+		landlock_log_denial(dom, &request1);
+	}
+	if (request2.access) {
+		request2.audit.u.path.dentry = new_dir->dentry;
+		landlock_log_denial(dom, &request2);
+	}
+
 	/*
 	 * This prioritizes EACCES over EXDEV for all actions, including
 	 * renames with RENAME_EXCHANGE.
@@ -1546,6 +1584,7 @@  static int hook_file_open(struct file *const file)
 		optional_access;
 	const struct landlock_ruleset *const dom = landlock_match_ruleset(
 		landlock_cred(file->f_cred)->domain, any_fs);
+	struct landlock_request request = {};
 
 	if (!dom)
 		return 0;
@@ -1571,7 +1610,7 @@  static int hook_file_open(struct file *const file)
 		    dom, &file->f_path,
 		    landlock_init_layer_masks(dom, 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 {
 		unsigned long access_bit;
@@ -1601,6 +1640,9 @@  static int hook_file_open(struct file *const file)
 	if ((open_access_request & allowed_access) == open_access_request)
 		return 0;
 
+	/* Sets access to reflect the actual request. */
+	request.access = open_access_request;
+	landlock_log_denial(dom, &request);
 	return -EACCES;
 }