Implement LANDLOCK_ADD_RULE_QUIET
Subsystems (3)
v3 cover Implement LANDLOCK_ADD_RULE_QUIET
Hi,
This is the v3 of the "quiet flag" series, implementing the feature as proposed in [1].
v2: https://lore.kernel.org/all/[email protected]/
v1: https://lore.kernel.org/all/[email protected]/
Not much has changed in the actual functionality except various comment, typing, asserts and general style fixes based on feedback. The major new thing here is tests (a bit of KUnit squashed into the optional access commit, a lot of selftests especially in fs_tests.c).
This series is still missing:
- Tests for scopes (ptrace, mounts, signals, abstract sockets)
- Some trivial edge cases (quiet_access > handled_access, setting quiet when quiet_access == 0)
However it turns out the tests I've already finished has grown quite large, and I expect many feedback on it. As such I thought I would send this v3 now anyway, as there is probably already a lot to review. The added fs_tests should exercise code path for optional and non-optional access, renames, and mountpoint and disconnected directory handling. I will add the above missing bits to v4.
New patches since v2:
- "selftests/landlock: add tests for quiet flag with net rules"
- "selftests/landlock: add tests for quiet flag with fs rules"
Removed:
- "Implement quiet for optional accesses"
(squashed into "landlock: Suppress logging when quiet flag is present")
Old feature summary below:
The quiet flag allows a sandboxer to suppress audit logs for uninteresting denials. The flag can be set on objects and inherits downward in the filesystem hierarchy. On a denial, the youngest denying layer's quiet flag setting decides whether to audit. The motivation for this feature is to reduce audit noise, and also prepare for a future supervisor feature which will use this bit to suppress supervisor notifications.
This patch introduces a new quiet access mask in the ruleset_attr, which gets eventually stored in the hierarchy. This allows the user to specify which access should be affected by quiet bits. One can then, for example, make it such that read accesses to certain files are not audited (but still denied), but all writes are still audited, regardless of location.
The sandboxer is extended to show example usage of this feature, supporting quieting filesystem, network and scope accesses.
Demo:
/# LL_FS_RO=/usr LL_FS_RW= LL_FORCE_LOG=1 LL_FS_QUIET=/dev:/tmp:/etc LL_FS_QUIET_ACCESS=r ./sandboxer bash
...
audit: type=1423 audit(1759680175.562:195): domain=15bb25f6b blockers=fs.write_file,fs.read_file path="/dev/tty" dev="devtmpfs" ino=11
^^^^^^^^
# note: because write is not quieted, we see the above line. blockers
# contains read as well since that's the originally requested access.
audit: type=1424 audit(1759680175.562:195): domain=15bb25f6b status=allocated mode=enforcing pid=616 uid=0 exe="/sandboxer" comm="sandboxer"
audit: type=1300 audit(1759680175.562:195): arch=c000003e syscall=257 success=no exit=-13 a0=ffffffffffffff9c a1=5565c86113d1 a2=802 a3=0 items=0 ppid=605 pid=616 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="bash" exe="/usr/bin/bash" key=(null)
audit: type=1327 audit(1759680175.562:195): proctitle="bash"
bash: cannot set terminal process group (605): Inappropriate ioctl for device
bash: no job control in this shell
bash: /etc/bash.bashrc: Permission denied
audit: type=1423 audit(1759680175.570:196): domain=15bb25f6b blockers=fs.read_file path="/.bash_history" dev="virtiofs" ino=36963
^^^^^^^^
# read outside /dev:/tmp:/etc - not quieted
audit: type=1300 audit(1759680175.570:196): arch=c000003e syscall=257 success=no exit=-13 a0=ffffffffffffff9c a1=5565c868e400 a2=0 a3=0 items=0 ppid=605 pid=616 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="bash" exe="/usr/bin/bash" key=(null)
audit: type=1327 audit(1759680175.570:196): proctitle="bash"
audit: type=1423 audit(1759680175.570:197): domain=15bb25f6b blockers=fs.read_file path="/.bash_history" dev="virtiofs" ino=36963
audit: type=1300 audit(1759680175.570:197): arch=c000003e syscall=257 success=no exit=-13 a0=ffffffffffffff9c a1=5565c868e400 a2=0 a3=0 items=0 ppid=605 pid=616 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="bash" exe="/usr/bin/bash" key=(null)
audit: type=1327 audit(1759680175.570:197): proctitle="bash"
bash-5.2# head /etc/passwd
head: cannot open '/etc/passwd' for reading: Permission denied
^^^^^^^^
# reads to /etc are quieted
bash-5.2# echo evil >> /etc/passwd
bash: /etc/passwd: Permission denied
audit: type=1423 audit(1759680227.030:198): domain=15bb25f6b blockers=fs.write_file path="/etc/passwd" dev="virtiofs" ino=790
^^^^^^^^
# writes are not quieted
audit: type=1300 audit(1759680227.030:198): arch=c000003e syscall=257 success=no exit=-13 a0=ffffffffffffff9c a1=5565c86ab030 a2=441 a3=1b6 items=0 ppid=605 pid=616 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="bash" exe="/usr/bin/bash" key=(null)
audit: type=1327 audit(1759680227.030:198): proctitle="bash"Design:
- The user can set the quiet flag for a layer on any part of the fs hierarchy (whether it allows any access on it or not), and the flag inherits down (no support for "cancelling" the inheritance of the flag in specific subdirectories).
- The youngest layer that denies a request gets to decide whether the denial is audited or not. This means that a compromised binary, for example, cannot "turn off" Landlock auditing when it tries to access files, unless it denies access to the files itself. There is some debate to be had on whether, if a parent layer sets the quiet flag, but the request is denied by a deeper layer, whether Landlock should still audit anyway (since the rule author of the child layer likely did not expect the denial, so it would be good diagnostic). The current approach is to ignore the quiet on the parent layer and audit anyway.
[1]: https://github.com/landlock-lsm/linux/issues/44#issuecomment-2876500918
Kind regards,
Tingmao
Tingmao Wang (8):
landlock: Add a place for flags to layer rules
landlock: Add API support and docs for the quiet flags
landlock: Suppress logging when quiet flag is present
landlock: Fix wrong type usage
samples/landlock: Add quiet flag support to sandboxer selftests/landlock: Replace hard-coded 16 with a constant selftests/landlock: add tests for quiet flag with fs rules selftests/landlock: add tests for quiet flag with net rules
include/uapi/linux/landlock.h | 64 + samples/landlock/sandboxer.c | 133 +- security/landlock/access.h | 5 + security/landlock/audit.c | 257 +- security/landlock/audit.h | 4 +- security/landlock/domain.c | 33 + security/landlock/domain.h | 10 + security/landlock/fs.c | 144 +- security/landlock/fs.h | 19 +- security/landlock/net.c | 11 +- security/landlock/net.h | 3 +- security/landlock/ruleset.c | 17 +- security/landlock/ruleset.h | 39 +- security/landlock/syscalls.c | 72 +- security/landlock/task.c | 12 +- tools/testing/selftests/landlock/audit_test.c | 2 +- tools/testing/selftests/landlock/base_test.c | 4 +- tools/testing/selftests/landlock/common.h | 2 + tools/testing/selftests/landlock/fs_test.c | 2238 ++++++++++++++++- tools/testing/selftests/landlock/net_test.c | 121 +- 20 files changed, 3058 insertions(+), 132 deletions(-)
base-commit: 72fb0170ef1f45addf726319c52a0562b6913707
Signature · 1 line
2.51.1
PATCH v3 1/8 landlock: Add a place for flags to layer rules
To avoid unnecessarily increasing the size of struct landlock_layer, we make the layer level a u8 and use the space to store the flags struct.
---
Changes since v2:
- Comment changes, move local variables, simplify if branch
Changes since v1:
- Comment changes
- Rebased to include disconnected directory handling changes on mic/next and add backing up of collected_rule_flags.
security/landlock/fs.c | 95 ++++++++++++++++++++++++++----------- security/landlock/net.c | 3 +- security/landlock/ruleset.c | 7 ++- security/landlock/ruleset.h | 26 +++++++++- 4 files changed, 99 insertions(+), 32 deletions(-)
security/landlock/fs.c +67 -28
@@ -757,10 +757,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], + struct collected_rule_flags *const rule_flags_parent1, 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], + struct collected_rule_flags *const rule_flags_parent2, struct landlock_request *const log_request_parent2, struct dentry *const dentry_child2) {
@@ -775,6 +777,7 @@ static bool is_access_to_paths_allowed( _layer_masks_parent2_bkp[LANDLOCK_NUM_ACCESS_FS ];layer_mask_t (*layer_masks_child1)[LANDLOCK_NUM_ACCESS_FS ] =NULL , (*layer_masks_child2)[LANDLOCK_NUM_ACCESS_FS ] =NULL ; +struct collected_rule_flags _rule_flag_parent1_bkp, _rule_flag_parent2_bkp;if (!access_request_parent1 && !access_request_parent2)return true;
@@ -800,6 +803,8 @@ static bool is_access_to_paths_allowed(*/ memcpy (&_layer_masks_parent2_bkp, layer_masks_parent2,sizeof (_layer_masks_parent2_bkp)); +memcpy (&_rule_flag_parent2_bkp, rule_flags_parent2, +sizeof (_rule_flag_parent2_bkp)); allowed_parent2 =is_layer_masks_allowed (layer_masks_parent2);/*
@@ -826,26 +831,38 @@ static bool is_access_to_paths_allowed(*/ memcpy (&_layer_masks_parent1_bkp, layer_masks_parent1,sizeof (_layer_masks_parent1_bkp)); +memcpy (&_rule_flag_parent1_bkp, rule_flags_parent1, +sizeof (_rule_flag_parent1_bkp)); allowed_parent1 =is_layer_masks_allowed (layer_masks_parent1); is_dom_check_bkp = is_dom_check;if (unlikely (dentry_child1)) { +/* +* Get the layer masks for the child dentries for use by domain +* check later. The rule_flags for child1 should have been +* included in rule_flags_parent1 already (cf. +* collect_domain_accesses), and is not relevant for domain check, +* so we don't have to pass it to landlock_unmask_layers. +*/ landlock_unmask_layers (find_rule (domain, dentry_child1),landlock_init_layer_masks ( domain,LANDLOCK_MASK_ACCESS_FS , &_layer_masks_child1,LANDLOCK_KEY_INODE ), - &_layer_masks_child1,ARRAY_SIZE (_layer_masks_child1)); + &_layer_masks_child1,ARRAY_SIZE (_layer_masks_child1), +NULL ); layer_masks_child1 = &_layer_masks_child1; child1_is_directory =d_is_dir (dentry_child1); }if (unlikely (dentry_child2)) { +/* See above comment for why NULL is passed as rule_flags_masks. */ landlock_unmask_layers (find_rule (domain, dentry_child2),landlock_init_layer_masks ( domain,LANDLOCK_MASK_ACCESS_FS , &_layer_masks_child2,LANDLOCK_KEY_INODE ), - &_layer_masks_child2,ARRAY_SIZE (_layer_masks_child2)); + &_layer_masks_child2,ARRAY_SIZE (_layer_masks_child2), +NULL ); layer_masks_child2 = &_layer_masks_child2; child2_is_directory =d_is_dir (dentry_child2); }
@@ -901,16 +918,18 @@ static bool is_access_to_paths_allowed( NULL :find_rule (domain, walker_path.dentry); - allowed_parent1 = allowed_parent1 || -landlock_unmask_layers ( - rule, access_masked_parent1, - layer_masks_parent1, -ARRAY_SIZE (*layer_masks_parent1)); - allowed_parent2 = allowed_parent2 || -landlock_unmask_layers ( - rule, access_masked_parent2, - layer_masks_parent2, -ARRAY_SIZE (*layer_masks_parent2)); + allowed_parent1 = + allowed_parent1 || +landlock_unmask_layers (rule, access_masked_parent1, + layer_masks_parent1, +ARRAY_SIZE (*layer_masks_parent1), + rule_flags_parent1); + allowed_parent2 = + allowed_parent2 || +landlock_unmask_layers (rule, access_masked_parent2, + layer_masks_parent2, +ARRAY_SIZE (*layer_masks_parent2), + rule_flags_parent2);/* Stops when a rule from each layer grants access. */ if (allowed_parent1 && allowed_parent2) {
@@ -947,10 +966,16 @@ static bool is_access_to_paths_allowed(memcpy (&_layer_masks_parent1_bkp, layer_masks_parent1,sizeof (_layer_masks_parent1_bkp)); +memcpy (&_rule_flag_parent1_bkp, + rule_flags_parent1, +sizeof (_rule_flag_parent1_bkp));if (layer_masks_parent2) {memcpy (&_layer_masks_parent2_bkp, layer_masks_parent2,sizeof (_layer_masks_parent2_bkp)); +memcpy (&_rule_flag_parent2_bkp, + rule_flags_parent2, +sizeof (_rule_flag_parent2_bkp)); is_dom_check_bkp = is_dom_check; }
@@ -999,11 +1024,15 @@ static bool is_access_to_paths_allowed(*/ memcpy (layer_masks_parent1, &_layer_masks_parent1_bkp,sizeof (_layer_masks_parent1_bkp)); +memcpy (rule_flags_parent1, &_rule_flag_parent1_bkp, +sizeof (_rule_flag_parent1_bkp)); allowed_parent1 =is_layer_masks_allowed (&_layer_masks_parent1_bkp);if (layer_masks_parent2) {memcpy (layer_masks_parent2, &_layer_masks_parent2_bkp,sizeof (_layer_masks_parent2_bkp)); +memcpy (rule_flags_parent2, &_rule_flag_parent2_bkp, +sizeof (_rule_flag_parent2_bkp)); allowed_parent2 =is_layer_masks_allowed ( &_layer_masks_parent2_bkp);
@@ -1063,6 +1092,7 @@ static int current_check_access_path(const struct path *const path,landlock_get_applicable_subject (current_cred (), masks,NULL );layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS ] = {};struct landlock_request request = {}; +struct collected_rule_flags rule_flags = {};if (!subject)return 0 ;
@@ -1071,8 +1101,8 @@ static int current_check_access_path(const struct path *const path, access_request, &layer_masks, LANDLOCK_KEY_INODE);if (is_access_to_paths_allowed (subject->domain, path, access_request, - &layer_masks, &request,NULL ,0 ,NULL , -NULL ,NULL )) + &layer_masks, &rule_flags, &request, +NULL ,0 ,NULL ,NULL ,NULL ,NULL ))return 0 ;landlock_log_denial (subject, &request);
@@ -1139,7 +1169,8 @@ static access_mask_t maybe_remove(const struct dentry *const dentry)static bool collect_domain_accesses (const struct landlock_ruleset *const domain,const struct path *const mnt_dir,struct dentry *dir, -layer_mask_t (*const layer_masks_dom)[LANDLOCK_NUM_ACCESS_FS ]) +layer_mask_t (*const layer_masks_dom)[LANDLOCK_NUM_ACCESS_FS ], +struct collected_rule_flags *const rule_flags) {access_mask_t access_dom;bool ret = false;
@@ -1158,9 +1189,9 @@ static bool collect_domain_accesses(struct dentry *parent_dentry;/* Gets all layers allowing all domain accesses. */ -if (landlock_unmask_layers (find_rule (domain, dir), access_dom, - layer_masks_dom, -ARRAY_SIZE (*layer_masks_dom))) { +if (landlock_unmask_layers ( +find_rule (domain, dir), access_dom, layer_masks_dom, +ARRAY_SIZE (*layer_masks_dom), rule_flags)) {/* * Before allowing this side of the access request, checks that the * walk was not in a disconnected directory.
@@ -1269,6 +1300,8 @@ static int current_check_refer_path(struct dentry *const old_dentry,layer_mask_t layer_masks_parent1[LANDLOCK_NUM_ACCESS_FS ] = {}, layer_masks_parent2[LANDLOCK_NUM_ACCESS_FS ] = {};struct landlock_request request1 = {}, request2 = {}; +struct collected_rule_flags rule_flags_parent1 = {}, + rule_flags_parent2 = {};if (!subject)return 0 ;
@@ -1300,10 +1333,10 @@ 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, &request1, -NULL ,0 ,NULL ,NULL ,NULL )) +if (is_access_to_paths_allowed ( + subject->domain, new_dir, access_request_parent1, + &layer_masks_parent1, &rule_flags_parent1, + &request1,NULL ,0 ,NULL ,NULL ,NULL ,NULL ))return 0 ;landlock_log_denial (subject, &request1);
@@ -1327,11 +1360,14 @@ static int current_check_refer_path(struct dentry *const old_dentry, old_dentry->d_parent;/* new_dir->dentry is equal to new_dentry->d_parent */ - allow_parent1 =collect_domain_accesses ( - subject->domain, &mnt_dir, old_parent, &layer_masks_parent1); + allow_parent1 =collect_domain_accesses (subject->domain, &mnt_dir, + old_parent, + &layer_masks_parent1, + &rule_flags_parent1); allow_parent2 =collect_domain_accesses (subject->domain, &mnt_dir, new_dir->dentry, - &layer_masks_parent2); + &layer_masks_parent2, + &rule_flags_parent2);if (allow_parent1 && allow_parent2)return 0 ;
@@ -1343,8 +1379,9 @@ 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, &request1, old_dentry, - access_request_parent2, &layer_masks_parent2, &request2, + &layer_masks_parent1, &rule_flags_parent1, &request1, + old_dentry, access_request_parent2, &layer_masks_parent2, + &rule_flags_parent2, &request2, exchange ? new_dentry :NULL ))return 0 ;
@@ -1747,6 +1784,7 @@ static int hook_file_open(struct file *const file)const struct landlock_cred_security *const subject =landlock_get_applicable_subject (file->f_cred, any_fs,NULL );struct landlock_request request = {}; +struct collected_rule_flags rule_flags = {};if (!subject)return 0 ;
@@ -1773,7 +1811,8 @@ 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, &request, NULL, 0, NULL, NULL, NULL)) { + &layer_masks, &rule_flags, &request, NULL, 0, NULL, NULL, + NULL, NULL)) { allowed_access = full_access_request; } else {unsigned long access_bit;
security/landlock/net.c +2 -1
@@ -48,6 +48,7 @@ static int current_check_access_socket(struct socket *const sock, {__be16 port;layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_NET ] = {}; +struct collected_rule_flags rule_flags = {};const struct landlock_rule *rule;struct landlock_id id = { .type =LANDLOCK_KEY_NET_PORT ,
@@ -179,7 +180,7 @@ static int current_check_access_socket(struct socket *const sock, access_request, &layer_masks, LANDLOCK_KEY_NET_PORT);if (landlock_unmask_layers (rule, access_request, &layer_masks, -ARRAY_SIZE (layer_masks))) +ARRAY_SIZE (layer_masks), &rule_flags))return 0 ; audit_net.family = address->sa_family;
security/landlock/ruleset.c +6 -1
@@ -624,7 +624,8 @@ landlock_find_rule(const struct landlock_ruleset *const ruleset,bool landlock_unmask_layers (const struct landlock_rule *const rule,const access_mask_t access_request,layer_mask_t (*const layer_masks)[], -const size_t masks_array_size) +const size_t masks_array_size, +struct collected_rule_flags *const rule_flags) {size_t layer_level;
@@ -651,6 +652,10 @@ bool landlock_unmask_layers(const struct landlock_rule *const rule,unsigned long access_bit;bool is_empty; +/* Collect rule flags for each layer. */ +if (rule_flags && layer->flags.quiet) + rule_flags->quiet_masks |= layer_bit; +/* * Records in @layer_masks which layer grants access to each requested * access: bit cleared if the related layer grants access.
security/landlock/ruleset.h +24 -2
@@ -29,7 +29,18 @@ struct landlock_layer {/** * @level: Position of this layer in the layer stack. Starts from 1. */ -u16 level; +u8 level; +/** +* @flags: Bitfield for special flags attached to this rule. +*/ +struct { +/** +* @quiet: Suppresses denial audit logs for the object covered by +* this rule in this domain. For filesystem rules, this inherits +* down the file hierarchy. +*/ +bool quiet:1 ; + } flags;/** * @access: Bitfield of allowed actions on the kernel object. They are * relative to the object type (e.g. %LANDLOCK_ACTION_FS_READ).
@@ -37,6 +48,16 @@ struct landlock_layer {access_mask_t access; }; +/** +* struct collected_rule_flags - Hold accumulated flags for each layer. +*/ +struct collected_rule_flags { +/** +* @quiet_masks: Layers for which the quiet flag is effective. +*/ +layer_mask_t quiet_masks; +}; +/** * union landlock_key - Key of a ruleset's red-black tree */
@@ -304,7 +325,8 @@ landlock_get_scope_mask(const struct landlock_ruleset *const ruleset,bool landlock_unmask_layers (const struct landlock_rule *const rule,const access_mask_t access_request,layer_mask_t (*const layer_masks)[], -const size_t masks_array_size); +const size_t masks_array_size, +struct collected_rule_flags *const rule_flags); access_mask_t landlock_init_layer_masks(const struct landlock_ruleset *const domain,
Signature · 1 line
2.51.1
PATCH v3 2/8 landlock: Add API support and docs for the quiet flags
In reply to Tingmao WangAs for kselftests, for now we just change add_rule_checks_ordering to use a different invalid flag. I will add tests for the quiet flag in a later version.
According to pahole, even after adding the struct access_masks quiet_masks in struct landlock_hierarchy, the u32 log_* bitfield still only has a size of 2 bytes, so there's minimal wasted space.
---
Changes since v2:
- Updated docs from Mickaël's suggestions.
Changes since v1:
- Per suggestion, added support for quieting only certain access bits, controlled by extra quiet_access_* fields in the ruleset_attr.
- Added docs for the extra fields and made updates to doc changes in v1. In particular, call out that the effect of LANDLOCK_ADD_RULE_QUIET is independent from the access bits passed in rule_attr
- landlock_add_rule will return -EINVAL when LANDLOCK_ADD_RULE_QUIET is used but the ruleset does not have any quiet access bits set for the given rule type.
- ABI version bump to v8
- Syntactic and comment changes per suggestion.
include/uapi/linux/landlock.h | 64 +++++++++++++++++ security/landlock/domain.h | 5 ++ security/landlock/fs.c | 4 +- security/landlock/fs.h | 2 +- security/landlock/net.c | 5 +- security/landlock/net.h | 3 +- security/landlock/ruleset.c | 10 ++- security/landlock/ruleset.h | 8 ++- security/landlock/syscalls.c | 72 +++++++++++++++----- tools/testing/selftests/landlock/base_test.c | 4 +- 10 files changed, 150 insertions(+), 27 deletions(-)
include/uapi/linux/landlock.h +64 -0
@@ -32,6 +32,19 @@* *handle* a wide range or all access rights that they know about at build time * (and that they have tested with a kernel that supported them all). * +* @quiet_access_fs and @quiet_access_net are bitmasks of actions for +* which a denial by this layer will not trigger an audit log if the +* corresponding object (or its children, for filesystem rules) is marked +* with the "quiet" bit via %LANDLOCK_ADD_RULE_QUIET, even if logging +* would normally take place per landlock_restrict_self() flags. +* quiet_scoped is similar, except that it does not require marking any +* objects as quiet - if the ruleset is created with any bits set in +* quiet_scoped, then denial of such scoped resources will not trigger any +* log. These 3 fields are available since Landlock ABI version 8. +* +* @quiet_access_fs, @quiet_access_net and @quiet_scoped must be a subset +* of @handled_access_fs, @handled_access_net and @scoped respectively. +* * This structure can grow in future Landlock versions. */ struct landlock_ruleset_attr {
@@ -51,6 +64,24 @@ struct landlock_ruleset_attr {* resources (e.g. IPCs). */ __u64 scoped; + +/* Since ABI 8: */ + +/** +* @quiet_access_fs: Bitmask of filesystem actions which should not be +* audit logged if per-object quiet flag is set. +*/ +__u64 quiet_access_fs; +/** +* @quiet_access_net: Bitmask of network actions which should not be +* audit logged if per-object quiet flag is set. +*/ +__u64 quiet_access_net; +/** +* @quiet_scoped: Bitmask of scoped actions which should not be audit +* logged. +*/ +__u64 quiet_scoped; };/**
@@ -69,6 +100,39 @@ struct landlock_ruleset_attr {#define LANDLOCK_CREATE_RULESET_ERRATA (1U << 1)/* clang-format on */ +/** +* DOC: landlock_add_rule_flags +* +* **Flags** +* +* %LANDLOCK_ADD_RULE_QUIET +* Together with the quiet_* fields in struct landlock_ruleset_attr, +* this flag controls whether Landlock will log audit messages when +* access to the objects covered by this rule is denied by this layer. +* +* If audit logging is enabled, when Landlock denies an access, it will +* suppress the audit log if all of the following are true: +* +* - This layer is the innermost layer that denied the access; +* - All requested accesses are part of the quiet_* fields in the +* related struct landlock_ruleset_attr; +* - The object (or one of its parents, for filesystem rules) is +* marked as "quiet" via %LANDLOCK_ADD_RULE_QUIET. +* +* Because logging is only suppressed by a layer if the layer denies +* access, a sandboxed program cannot use this flag to "hide" access +* denials, without denying itself the access in the first place. +* +* The effect of this flag does not depend on the value of +* allowed_access in the passed in rule_attr. When this flag is +* present, the caller is also allowed to pass in an empty +* allowed_access. +*/ + +/* clang-format off */ +#define LANDLOCK_ADD_RULE_QUIET (1U << 0) +/* clang-format on */ +/** * DOC: landlock_restrict_self_flags *
security/landlock/domain.h +5 -0
@@ -114,6 +114,11 @@ struct landlock_hierarchy {* %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON. Set to false by default. */ log_new_exec :1 ; +/** +* @quiet_masks: Bitmasks of access that should be quieted (i.e. not +* logged) if the related object is marked as quiet. +*/ +struct access_masks quiet_masks;#endif /* CONFIG_AUDIT */ };
security/landlock/fs.c +2 -2
@@ -322,7 +322,7 @@ static struct landlock_object *get_inode_object(struct inode *const inode)*/ int landlock_append_fs_rule (struct landlock_ruleset *const ruleset,const struct path *const path, -access_mask_t access_rights) +access_mask_t access_rights,const int flags) {int err;struct landlock_id id = {
@@ -343,7 +343,7 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,if (IS_ERR (id.key.object))return PTR_ERR (id.key.object);mutex_lock (&ruleset->lock); - err =landlock_insert_rule (ruleset, id, access_rights); + err =landlock_insert_rule (ruleset, id, access_rights, flags);mutex_unlock (&ruleset->lock);/* * No need to check for an error because landlock_insert_rule()
security/landlock/fs.h +1 -1
@@ -126,6 +126,6 @@ __init void landlock_add_fs_hooks(void);int landlock_append_fs_rule (struct landlock_ruleset *const ruleset,const struct path *const path, -access_mask_t access_hierarchy); +access_mask_t access_hierarchy,const int flags);#endif /* _SECURITY_LANDLOCK_FS_H */
security/landlock/net.c +3 -2
@@ -20,7 +20,8 @@#include "ruleset.h" int landlock_append_net_rule (struct landlock_ruleset *const ruleset, -const u16 port,access_mask_t access_rights) +const u16 port,access_mask_t access_rights, +const int flags) {int err;const struct landlock_id id = {
@@ -35,7 +36,7 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset, ~landlock_get_net_access_mask (ruleset,0 );mutex_lock (&ruleset->lock); - err =landlock_insert_rule (ruleset, id, access_rights); + err =landlock_insert_rule (ruleset, id, access_rights, flags);mutex_unlock (&ruleset->lock);return err;
security/landlock/net.h +2 -1
@@ -16,7 +16,8 @@__init void landlock_add_net_hooks (void );int landlock_append_net_rule (struct landlock_ruleset *const ruleset, -const u16 port,access_mask_t access_rights); +const u16 port,access_mask_t access_rights, +const int flags);#else /* IS_ENABLED(CONFIG_INET) */ static inline void landlock_add_net_hooks (void ) {
security/landlock/ruleset.c +9 -1
@@ -21,6 +21,7 @@#include <linux/slab.h> #include <linux/spinlock.h> #include <linux/workqueue.h> +#include <uapi/linux/landlock.h> #include "access.h" #include "audit.h"
@@ -255,6 +256,7 @@ static int insert_rule(struct landlock_ruleset *const ruleset,if (WARN_ON_ONCE (this->layers[0 ].level !=0 ))return -EINVAL ; this->layers[0 ].access |= (*layers)[0 ].access; + this->layers[0 ].flags.quiet |= (*layers)[0 ].flags.quiet;return 0 ; }
@@ -305,12 +307,15 @@ static void build_check_layer(void)/* @ruleset must be locked by the caller. */ int landlock_insert_rule (struct landlock_ruleset *const ruleset,const struct landlock_id id, -const access_mask_t access) +const access_mask_t access,const int flags) {struct landlock_layer layers[] = { { .access = access,/* When @level is zero, insert_rule() extends @ruleset. */ .level =0 , + .flags = { + .quiet = !!(flags &LANDLOCK_ADD_RULE_QUIET ), + } } };build_check_layer ();
@@ -351,6 +356,7 @@ static int merge_tree(struct landlock_ruleset *const dst,return -EINVAL ; layers[0 ].access = walker_rule->layers[0 ].access; + layers[0 ].flags = walker_rule->layers[0 ].flags; err =insert_rule (dst, id, &layers,ARRAY_SIZE (layers));if (err)
@@ -581,6 +587,8 @@ landlock_merge_ruleset(struct landlock_ruleset *const parent,if (err)return ERR_PTR (err); + new_dom->hierarchy->quiet_masks = ruleset->quiet_masks; +return no_free_ptr (new_dom); }
security/landlock/ruleset.h +7 -1
@@ -193,6 +193,12 @@ struct landlock_ruleset {* non-merged ruleset (i.e. not a domain). */ u32 num_layers; +/** +* @quiet_masks: Stores the quiet flags for an unmerged +* ruleset. For a merged domain, this is stored in each +* layer's struct landlock_hierarchy instead. +*/ +struct access_masks quiet_masks;/** * @access_masks: Contains the subset of filesystem and * network actions that are restricted by a ruleset.
@@ -223,7 +229,7 @@ DEFINE_FREE(landlock_put_ruleset, struct landlock_ruleset *,int landlock_insert_rule (struct landlock_ruleset *const ruleset,const struct landlock_id id, -const access_mask_t access); +const access_mask_t access,const int flags);struct landlock_ruleset * landlock_merge_ruleset(struct landlock_ruleset *const parent,
security/landlock/syscalls.c +55 -17
189 lines not on this page · raw message
tools/testing/selftests/landlock/base_test.c +2 -2
@@ -76,7 +76,7 @@ TEST(abi_version)const struct landlock_ruleset_attr ruleset_attr = { .handled_access_fs =LANDLOCK_ACCESS_FS_READ_FILE , }; -ASSERT_EQ (7 ,landlock_create_ruleset (NULL ,0 , +ASSERT_EQ (8 ,landlock_create_ruleset (NULL ,0 ,LANDLOCK_CREATE_RULESET_VERSION )); ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
@@ -201,7 +201,7 @@ TEST(add_rule_checks_ordering)ASSERT_LE (0 , ruleset_fd);/* Checks invalid flags. */ -ASSERT_EQ (-1 ,landlock_add_rule (-1 ,0 ,NULL ,1 )); +ASSERT_EQ (-1 ,landlock_add_rule (-1 ,0 ,NULL ,100 ));ASSERT_EQ (EINVAL , errno);/* Checks invalid ruleset FD. */
Signature · 1 line
2.51.1
Re: [PATCH v3 2/8] landlock: Add API support and docs for the quiet flags
Quoted 3 lines · Tingmao Wang
As for kselftests, for now we just change
add_rule_checks_orderingto use a different invalid flag. I will add tests for the quiet flag in a later version.
This message is out of date. Will remove in v4. (Also will remember to put these sort of things under ---)
PATCH v3 3/8 landlock: Suppress logging when quiet flag is present
In reply to Tingmao WangThe quietness behaviour is as documented in the previous patch.
For optional accesses, since the existing deny_masks can only store 2x4bit of layer index, with no way to represent "no layer", we need to either expand it or have another field to correctly handle quieting of those. This commit uses the latter approach - we add another field to store which optional access (of the 2) are covered by quiet rules in their respective layers as stored in deny_masks.
We can avoid making struct landlock_file_security larger by converting the existing fown_layer to a 4bit field. This commit does that, and adds test to ensure that it is large enough for LANDLOCK_MAX_NUM_LAYERS-1.
---
Changes since v2:
- Renamed patch title from "Check for quiet flag in landlock_log_denial" to this given the growth.
- Moved quiet bit check after domain_exec check
- Rename, style and comment fixes suggested by Mickaël.
- Squashed patch 6/6 from v2 "Implement quiet for optional accesses" into this one. Changes to that below:
- Refactor the quiet flag setting in get_layer_from_deny_masks() to be more clear.
- Add KUnit tests
- Fix comments, add WARN_ON_ONCE, use __const_hweight64() as suggested by review
- Move build_check_file_security to fs.c
- Use a typedef for quiet_optional_accesses, add static_assert, and improve docs on landlock_get_quiet_optional_accesses.
Changes since v1:
- Supports the new quiet access masks.
- Support quieting scope requests (but not ptrace and attempted mounting for now)
security/landlock/access.h | 5 + security/landlock/audit.c | 255 +++++++++++++++++++++++++++++++++--- security/landlock/audit.h | 4 +- security/landlock/domain.c | 33 +++++ security/landlock/domain.h | 5 + security/landlock/fs.c | 45 +++++-- security/landlock/fs.h | 17 ++- security/landlock/net.c | 3 +- security/landlock/ruleset.h | 5 + security/landlock/task.c | 12 +- 10 files changed, 344 insertions(+), 40 deletions(-)
security/landlock/access.h +5 -0
@@ -97,4 +97,9 @@ landlock_upgrade_handled_access_masks(struct access_masks access_masks) return access_masks; } +/* A bitmask that is large enough to hold set of optional accesses. */ +typedef u8 optional_access_t ; +static_assert (BITS_PER_TYPE (optional_access_t) >= +HWEIGHT (_LANDLOCK_ACCESS_FS_OPTIONAL)); +#endif /* _SECURITY_LANDLOCK_ACCESS_H */
security/landlock/audit.c +239 -16
341 lines not on this page · raw message
security/landlock/audit.h +3 -1
@@ -49,6 +49,7 @@ struct landlock_request {/* Required fields for requests with deny masks. */ const access_mask_t all_existing_optional_access;deny_masks_t deny_masks; +u8 quiet_optional_accesses; };#ifdef CONFIG_AUDIT
@@ -56,7 +57,8 @@ struct landlock_request {void landlock_log_drop_domain (const struct landlock_hierarchy *const hierarchy);void landlock_log_denial (const struct landlock_cred_security *const subject, -const struct landlock_request *const request); +const struct landlock_request *const request, +const struct collected_rule_flags rule_flags);#else /* CONFIG_AUDIT */
security/landlock/domain.c +33 -0
@@ -212,6 +212,39 @@ landlock_get_deny_masks(const access_mask_t all_existing_optional_access,return deny_masks; } +/** +* landlock_get_quiet_optional_accesses - Get optional accesses which are +* "covered" by quiet rule flags. +* +* Returns a bitmask of which optional access are denied by layers for +* which rule_flags.quiet_masks has the corresponding bit set. +*/ +optional_access_t landlock_get_quiet_optional_accesses ( +const access_mask_t all_existing_optional_access, +const deny_masks_t deny_masks, +const struct collected_rule_flags rule_flags) +{ +const unsigned long access_opt = all_existing_optional_access; +size_t access_index =0 ; +unsigned long access_bit; +optional_access_t quiet_optional_accesses =0 ; + +/* 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)) { +const u8 layer = (deny_masks >> (access_index *4 )) & + (LANDLOCK_MAX_NUM_LAYERS -1 ); +const bool is_quiet = !!(rule_flags.quiet_masks &BIT (layer)); + +if (is_quiet) + quiet_optional_accesses |=BIT (access_index); + access_index++; + } +return quiet_optional_accesses; +} +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST static void test_landlock_get_deny_masks (struct kunit *const test)
security/landlock/domain.h +5 -0
@@ -130,6 +130,11 @@ landlock_get_deny_masks(const access_mask_t all_existing_optional_access,const layer_mask_t (*const layer_masks)[], size_t layer_masks_size); +optional_access_t landlock_get_quiet_optional_accesses ( +const access_mask_t all_existing_optional_access, +const deny_masks_t deny_masks, +const struct collected_rule_flags rule_flags); +int landlock_init_hierarchy_log (struct landlock_hierarchy *const hierarchy); static inline void
security/landlock/fs.c +36 -9
112 lines not on this page · raw message
security/landlock/fs.h +10 -7
@@ -63,11 +63,20 @@ struct landlock_file_security {* _LANDLOCK_ACCESS_FS_OPTIONAL). */ deny_masks_t deny_masks; +/** +* @quiet_optional_accesses: Stores which optional accesses are +* covered by quiet rules within the layer referred to in deny_masks, +* one access per bit. Does not take into account whether the quiet +* access bits are actually set in the layer's corresponding +* landlock_hierarchy. +*/ + optional_access_t quiet_optional_accesses + : HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL);/** * @fown_layer: Layer level of @fown_subject->domain with * LANDLOCK_SCOPE_SIGNAL. */ -u8 fown_layer; + u8 fown_layer:4;#endif /* CONFIG_AUDIT */ /**
@@ -82,12 +91,6 @@ struct landlock_file_security {#ifdef CONFIG_AUDIT -/* Makes sure all layers can be identified. */ -/* clang-format off */ -static_assert ((typeof_member (struct landlock_file_security ,fown_layer ))~0 >= -LANDLOCK_MAX_NUM_LAYERS ); -/* clang-format off */ -#endif /* CONFIG_AUDIT */ /**
security/landlock/net.c +2 -1
@@ -193,7 +193,8 @@ static int current_check_access_socket(struct socket *const sock, .access = access_request, .layer_masks = &layer_masks, .layer_masks_size =ARRAY_SIZE (layer_masks), - }); + }, + rule_flags);return -EACCES ; }
security/landlock/ruleset.h +5 -0
@@ -58,6 +58,11 @@ struct collected_rule_flags {layer_mask_t quiet_masks; }; +/** +* no_rule_flags - Convenience constant for an empty collected_rule_flags. +*/ +static const struct collected_rule_flags no_rule_flags = {}; +/** * union landlock_key - Key of a ruleset's red-black tree */
security/landlock/task.c +6 -6
@@ -115,7 +115,7 @@ static int hook_ptrace_access_check(struct task_struct *const child, .u.tsk = child, }, .layer_plus_one = parent_subject->domain->num_layers, - }); + }, no_rule_flags);return err; }
@@ -161,7 +161,7 @@ static int hook_ptrace_traceme(struct task_struct *const parent) .u.tsk = current, }, .layer_plus_one = parent_subject->domain->num_layers, - }); + }, no_rule_flags);return err; }
@@ -290,7 +290,7 @@ static int hook_unix_stream_connect(struct sock *const sock, }, }, .layer_plus_one = handle_layer + 1, - }); + }, no_rule_flags);return -EPERM ; }
@@ -327,7 +327,7 @@ static int hook_unix_may_send(struct socket *const sock, }, }, .layer_plus_one = handle_layer + 1, - }); + }, no_rule_flags);return -EPERM ; }
@@ -383,7 +383,7 @@ static int hook_task_kill(struct task_struct *const p, .u.tsk = p, }, .layer_plus_one = handle_layer + 1, - }); + }, no_rule_flags);return -EPERM ; }
@@ -426,7 +426,7 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,#ifdef CONFIG_AUDIT .layer_plus_one = landlock_file(fown->file)->fown_layer + 1,#endif /* CONFIG_AUDIT */ - }); + }, no_rule_flags);return -EPERM ; }
Signature · 1 line
2.51.1
PATCH v3 4/8 landlock: Fix wrong type usage
In reply to Tingmao WangI think, based on my best understanding, that this type is likely a typo (even though in the end both are u16)
---
Changes since v1:
- Added Fixes tag
security/landlock/audit.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
security/landlock/audit.c +1 -1
@@ -191,7 +191,7 @@ static size_t get_denied_layer(const struct landlock_ruleset *const domain,long youngest_layer =-1 ;for_each_set_bit (access_bit, &access_req, layer_masks_size) { -const access_mask_t mask = (*layer_masks)[access_bit]; +const layer_mask_t mask = (*layer_masks)[access_bit];long layer;if (!mask)
Signature · 1 line
2.51.1
PATCH v3 5/8 samples/landlock: Add quiet flag support to sandboxer
In reply to Tingmao WangAdds ability to set which access bits to quiet via LL_*_QUIET_ACCESS (FS, NET or SCOPED), and attach quiet flags to individual objects via LL_*_QUIET for FS and NET.
---
Changes since v2:
- Minor change to the above commit message.
Changes since v1:
- Added new environment variables to control which quiet access bits to set on the rule, and populate quiet_access_* from it.
- Added support for quieting net rules and scoped access. Renamed patch title.
- Increment ABI version
samples/landlock/sandboxer.c | 133 ++++++++++++++++++++++++++++++++--- 1 file changed, 124 insertions(+), 9 deletions(-)
samples/landlock/sandboxer.c +124 -9
@@ -58,9 +58,14 @@ static inline int landlock_restrict_self(const int ruleset_fd,#define ENV_FS_RO_NAME "LL_FS_RO"#define ENV_FS_RW_NAME "LL_FS_RW" +#define ENV_FS_QUIET_NAME "LL_FS_QUIET" +#define ENV_FS_QUIET_ACCESS_NAME "LL_FS_QUIET_ACCESS"#define ENV_TCP_BIND_NAME "LL_TCP_BIND"#define ENV_TCP_CONNECT_NAME "LL_TCP_CONNECT" +#define ENV_NET_QUIET_NAME "LL_NET_QUIET" +#define ENV_NET_QUIET_ACCESS_NAME "LL_NET_QUIET_ACCESS"#define ENV_SCOPED_NAME "LL_SCOPED" +#define ENV_SCOPED_QUIET_ACCESS_NAME "LL_SCOPED_QUIET_ACCESS"#define ENV_FORCE_LOG_NAME "LL_FORCE_LOG"#define ENV_DELIMITER ":"
@@ -116,7 +121,7 @@ static int parse_path(char *env_path, const char ***const path_list)/* clang-format on */ static int populate_ruleset_fs (const char *const env_var,const int ruleset_fd, -const __u64 allowed_access) +const __u64 allowed_access,bool quiet) {int num_paths, i, ret =1 ;char *env_path_name;
@@ -166,7 +171,8 @@ static int populate_ruleset_fs(const char *const env_var, const int ruleset_fd,if (!S_ISDIR (statbuf.st_mode)) path_beneath.allowed_access &=ACCESS_FILE ;if (landlock_add_rule (ruleset_fd,LANDLOCK_RULE_PATH_BENEATH , - &path_beneath,0 )) { + &path_beneath, + quiet ?LANDLOCK_ADD_RULE_QUIET :0 )) {fprintf (stderr,"Failed to update the ruleset with \"%s\": %s\n" , path_list[i],strerror (errno));
@@ -184,7 +190,7 @@ static int populate_ruleset_fs(const char *const env_var, const int ruleset_fd, }static int populate_ruleset_net (const char *const env_var,const int ruleset_fd, -const __u64 allowed_access) +const __u64 allowed_access,bool quiet) {int ret =1 ;char *env_port_name, *env_port_name_next, *strport;
@@ -212,7 +218,8 @@ static int populate_ruleset_net(const char *const env_var, const int ruleset_fd, } net_port.port = port;if (landlock_add_rule (ruleset_fd,LANDLOCK_RULE_NET_PORT , - &net_port,0 )) { + &net_port, + quiet ?LANDLOCK_ADD_RULE_QUIET :0 )) {fprintf (stderr,"Failed to update the ruleset with port \"%llu\": %s\n" , net_port.port,strerror (errno));
@@ -299,7 +306,55 @@ static bool check_ruleset_scope(const char *const env_var,/* clang-format on */ -#define LANDLOCK_ABI_LAST 7 +static int add_quiet_access (__u64 *const quiet_access, +const __u64 handled_access, +const char *const env_var,const bool default_all) +{ +char *env_quiet_access, *env_quiet_access_next, *str_access; + +if (default_all) + *quiet_access = handled_access; +else + *quiet_access =0 ; + + env_quiet_access =getenv (env_var); +if (!env_quiet_access) +return 0 ; + + env_quiet_access =strdup (env_quiet_access); + env_quiet_access_next = env_quiet_access; +unsetenv (env_var); + *quiet_access =0 ; + +while ((str_access =strsep (&env_quiet_access_next,ENV_DELIMITER ))) { +if (strcmp (str_access,"" ) ==0 ) +continue ; +else if (strcmp (str_access,"r" ) ==0 ) + *quiet_access |=ACCESS_FS_ROUGHLY_READ ; +else if (strcmp (str_access,"w" ) ==0 ) + *quiet_access |=ACCESS_FS_ROUGHLY_WRITE ; +else if (strcmp (str_access,"b" ) ==0 ) + *quiet_access |=LANDLOCK_ACCESS_NET_BIND_TCP ; +else if (strcmp (str_access,"c" ) ==0 ) + *quiet_access |=LANDLOCK_ACCESS_NET_CONNECT_TCP ; +else if (strcmp (str_access,"a" ) ==0 ) + *quiet_access |=LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET ; +else if (strcmp (str_access,"s" ) ==0 ) + *quiet_access |=LANDLOCK_SCOPE_SIGNAL ; +else { +fprintf (stderr,"Unknown quiet access \"%s\"\n" , + str_access); +free (env_quiet_access); +return -1 ; + } + } + +free (env_quiet_access); + *quiet_access &= handled_access; +return 0 ; +} + +#define LANDLOCK_ABI_LAST 8#define XSTR (s) #s#define STR (s) XSTR(s)
@@ -328,6 +383,20 @@ static const char help[] ="\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" +ENV_FS_QUIET_NAME " and " ENV_NET_QUIET_NAME ", both optional, can then be used " +"to make access to some denied paths or network ports not trigger audit logging.\n" +ENV_FS_QUIET_ACCESS_NAME " and " ENV_NET_QUIET_ACCESS_NAME " can be used to specify " +"which accesses should be quieted (defaults to all):\n" +"* " ENV_FS_QUIET_ACCESS_NAME ": file system accesses to quiet\n" +" - \"r\" to quiet all file/dir read accesses\n" +" - \"w\" to quiet all file/dir write accesses\n" +"* " ENV_NET_QUIET_ACCESS_NAME ": network accesses to quiet\n" +" - \"b\" to quiet bind denials\n" +" - \"c\" to quiet connect denials\n" +"In addition, " ENV_SCOPED_QUIET_ACCESS_NAME " can be set to quiet all denials for " +"scoped actions (defaults to none).\n" +" - \"a\" to quiet abstract unix socket denials\n" +" - \"s\" to quiet signal denials\n" "\n" "Example:\n" ENV_FS_RO_NAME "=\"${PATH}:/lib:/usr:/proc:/etc:/dev/urandom\" "
@@ -357,7 +426,12 @@ int main(const int argc, char *const argv[], char *const *const envp) LANDLOCK_ACCESS_NET_CONNECT_TCP, .scoped =LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL, + .quiet_access_fs = 0, + .quiet_access_net = 0, + .quiet_scoped = 0, }; + +bool quiet_supported = true;int supported_restrict_flags =LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON ;int set_restrict_flags =0 ;
@@ -444,6 +518,11 @@ int main(const int argc, char *const argv[], char *const *const envp)"provided by ABI version %d (instead of %d).\n" , LANDLOCK_ABI_LAST, abi); __attribute__((fallthrough)); +case 7 : +/* Don't add quiet flags for ABI < 8 later on. */ + quiet_supported = false; + + __attribute__((fallthrough));case LANDLOCK_ABI_LAST :break ;default :
@@ -490,6 +569,25 @@ int main(const int argc, char *const argv[], char *const *const envp)unsetenv (ENV_FORCE_LOG_NAME ); } +/* +* Add quiet for fs/net handled access bits. Doing this alone has no +* effect unless we later add quiet rules per FS_QUIET/NET_QUIET. +*/ +if (quiet_supported) { +if (add_quiet_access (&ruleset_attr.quiet_access_fs, + ruleset_attr.handled_access_fs, +ENV_FS_QUIET_ACCESS_NAME , true)) +return 1 ; +if (add_quiet_access (&ruleset_attr.quiet_access_net, + ruleset_attr.handled_access_net, +ENV_NET_QUIET_ACCESS_NAME , true)) +return 1 ; +if (add_quiet_access (&ruleset_attr.quiet_scoped, + ruleset_attr.scoped, +ENV_SCOPED_QUIET_ACCESS_NAME , false)) +return 1 ; + } + ruleset_fd =landlock_create_ruleset (&ruleset_attr,sizeof (ruleset_attr),0 );if (ruleset_fd <0 ) {
@@ -497,22 +595,39 @@ int main(const int argc, char *const argv[], char *const *const envp)return 1 ; } -if (populate_ruleset_fs (ENV_FS_RO_NAME , ruleset_fd, access_fs_ro)) { +if (populate_ruleset_fs (ENV_FS_RO_NAME , ruleset_fd, access_fs_ro, + false)) { goto err_close_ruleset; } -if (populate_ruleset_fs (ENV_FS_RW_NAME , ruleset_fd, access_fs_rw)) { +if (populate_ruleset_fs (ENV_FS_RW_NAME , ruleset_fd, access_fs_rw, + false)) { goto err_close_ruleset; } +/* Don't require this env to be present. */ +if (quiet_supported &&getenv (ENV_FS_QUIET_NAME )) { +if (populate_ruleset_fs (ENV_FS_QUIET_NAME , ruleset_fd,0 , + true)) { + goto err_close_ruleset; + } + }if (populate_ruleset_net (ENV_TCP_BIND_NAME , ruleset_fd, -LANDLOCK_ACCESS_NET_BIND_TCP )) { +LANDLOCK_ACCESS_NET_BIND_TCP , false)) { goto err_close_ruleset; }if (populate_ruleset_net (ENV_TCP_CONNECT_NAME , ruleset_fd, -LANDLOCK_ACCESS_NET_CONNECT_TCP )) { +LANDLOCK_ACCESS_NET_CONNECT_TCP , false)) { goto err_close_ruleset; } +/* Don't require this env to be present. */ +if (quiet_supported &&getenv (ENV_NET_QUIET_NAME )) { +if (populate_ruleset_net (ENV_NET_QUIET_NAME , ruleset_fd,0 , + true)) { + goto err_close_ruleset; + } + } +if (prctl (PR_SET_NO_NEW_PRIVS ,1 ,0 ,0 ,0 )) {perror ("Failed to restrict privileges" ); goto err_close_ruleset;
Signature · 1 line
2.51.1
PATCH v3 6/8 selftests/landlock: Replace hard-coded 16 with a constant
In reply to Tingmao WangThe next commit will reuse this number. Make it a shared constant to future-proof changes.
---
Changes since v2:
- New patch
tools/testing/selftests/landlock/audit_test.c | 2 +- tools/testing/selftests/landlock/common.h | 2 ++ tools/testing/selftests/landlock/fs_test.c | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-)
tools/testing/selftests/landlock/audit_test.c +1 -1
@@ -76,7 +76,7 @@ TEST_F(audit, layers) .scoped = LANDLOCK_SCOPE_SIGNAL, };int status, ruleset_fd, i; -__u64 (*domain_stack)[16 ]; +__u64 (*domain_stack)[LANDLOCK_MAX_NUM_LAYERS ];__u64 prev_dom =3 ;pid_t child;
tools/testing/selftests/landlock/common.h +2 -0
@@ -25,6 +25,8 @@/* TEST_F_FORK() should not be used for new tests. */ #define TEST_F_FORK (fixture_name, test_name) TEST_F(fixture_name, test_name) +#define LANDLOCK_MAX_NUM_LAYERS 16 +static const char bin_sandbox_and_launch[] ="./sandbox-and-launch" ;static const char bin_wait_pipe[] ="./wait-pipe" ;static const char bin_wait_pipe_sandbox[] ="./wait-pipe-sandbox" ;
tools/testing/selftests/landlock/fs_test.c +1 -1
@@ -1497,7 +1497,7 @@ TEST_F_FORK(layout0, max_layers)const int ruleset_fd =create_ruleset (_metadata,ACCESS_RW , rules);ASSERT_LE (0 , ruleset_fd); -for (i =0 ; i <16 ; i++) +for (i =0 ; i <LANDLOCK_MAX_NUM_LAYERS ; i++)enforce_ruleset (_metadata, ruleset_fd);for (i =0 ; i <2 ; i++) {
Signature · 1 line
2.51.1
PATCH v3 7/8 selftests/landlock: add tests for quiet flag with fs rules
In reply to Tingmao WangTest various interactions of the quiet flag with filesystem rules:
- Non-optional access (tested with open and rename).
- Optional access (tested with truncate and ioctl).
- Behaviour around mounts matches with normal Landlock rules.
- Behaviour around disconnected directories matches with normal Landlock rules (test expected behaviour of 9a868cdbe66a ("landlock: Fix handling of disconnected directories") applied to the collected quiet flag).
- Multiple layers works as expected.
---
Changes since v2:
- New patch
tools/testing/selftests/landlock/fs_test.c | 2236 +++++++++++++++++++- 1 file changed, 2225 insertions(+), 11 deletions(-)
tools/testing/selftests/landlock/fs_test.c +2225 -11
@@ -718,11 +718,15 @@ TEST_F_FORK(layout1, rule_with_unhandled_access)static void add_path_beneath (struct __test_metadata *const _metadata,const int ruleset_fd,const __u64 allowed_access, -const char *const path) +const char *const path,bool quiet) {struct landlock_path_beneath_attr path_beneath = { .allowed_access = allowed_access, }; +__u32 flags =0 ; + +if (quiet) + flags |=LANDLOCK_ADD_RULE_QUIET ; path_beneath.parent_fd =open (path,O_PATH |O_CLOEXEC );ASSERT_LE (0 , path_beneath.parent_fd)
@@ -731,7 +735,7 @@ static void add_path_beneath(struct __test_metadata *const _metadata, strerror(errno)); }ASSERT_EQ (0 ,landlock_add_rule (ruleset_fd,LANDLOCK_RULE_PATH_BENEATH , - &path_beneath, 0)) + &path_beneath, flags)) {TH_LOG ("Failed to update the ruleset with \"%s\": %s" , path,strerror (errno));
@@ -786,7 +790,7 @@ static int create_ruleset(struct __test_metadata *const _metadata,continue ;add_path_beneath (_metadata, ruleset_fd, rules[i].access, - rules[i].path); + rules[i].path, false); }return ruleset_fd; }
@@ -1364,7 +1368,7 @@ TEST_F_FORK(layout1, inherit_subset)* ANDed with the previous ones. */ add_path_beneath (_metadata, ruleset_fd,LANDLOCK_ACCESS_FS_WRITE_FILE , - dir_s1d2); + dir_s1d2, false);/* * According to ruleset_fd, dir_s1d2 should now have the * LANDLOCK_ACCESS_FS_READ_FILE and LANDLOCK_ACCESS_FS_WRITE_FILE
@@ -1396,7 +1400,7 @@ TEST_F_FORK(layout1, inherit_subset)* Try to get more privileges by adding new access rights to the parent * directory: dir_s1d1. */ -add_path_beneath (_metadata, ruleset_fd,ACCESS_RW , dir_s1d1); +add_path_beneath (_metadata, ruleset_fd,ACCESS_RW , dir_s1d1, false);enforce_ruleset (_metadata, ruleset_fd);/* Same tests and results as above. */
@@ -1419,7 +1423,7 @@ TEST_F_FORK(layout1, inherit_subset)* that there was no rule tied to it before. */ add_path_beneath (_metadata, ruleset_fd,LANDLOCK_ACCESS_FS_WRITE_FILE , - dir_s1d3); + dir_s1d3, false);enforce_ruleset (_metadata, ruleset_fd);ASSERT_EQ (0 ,close (ruleset_fd));
@@ -1472,7 +1476,7 @@ TEST_F_FORK(layout1, inherit_superset)add_path_beneath (_metadata, ruleset_fd,LANDLOCK_ACCESS_FS_READ_FILE |LANDLOCK_ACCESS_FS_READ_DIR , - dir_s1d2); + dir_s1d2, false);enforce_ruleset (_metadata, ruleset_fd);ASSERT_EQ (0 ,close (ruleset_fd));
@@ -4211,7 +4215,7 @@ static int ioctl_error(struct __test_metadata *const _metadata, int fd, unsigned int cmd) {char buf[128 ];/* sufficiently large */ -int res, stdinbak_fd; +int res, stdinbak_fd, err;/* * Depending on the IOCTL command, parts of the zeroed-out buffer might
@@ -4226,13 +4230,14 @@ static int ioctl_error(struct __test_metadata *const _metadata, int fd,/* Invokes the IOCTL with a zeroed-out buffer. */ bzero (&buf,sizeof (buf)); res =ioctl (fd, cmd, &buf); + err = errno;/* Restores the old FD 0 and closes the backup FD. */ ASSERT_EQ (0 ,dup2 (stdinbak_fd,0 ));ASSERT_EQ (0 ,close (stdinbak_fd));if (res <0 ) -return errno; +return err;return 0 ; }
@@ -4579,6 +4584,7 @@ FIXTURE(layout1_bind) {};static const char bind_dir_s1d3[] =TMP_DIR "/s2d1/s2d2/s1d3" ;static const char bind_file1_s1d3[] =TMP_DIR "/s2d1/s2d2/s1d3/f1" ; +static const char bind_file2_s1d3[] =TMP_DIR "/s2d1/s2d2/s1d3/f2" ;/* Move targets for disconnected path tests. */ static const char dir_s4d1[] =TMP_DIR "/s4d1" ;
@@ -6927,8 +6933,8 @@ static int matches_log_fs_extra(struct __test_metadata *const _metadata,return -E2BIG ;/* -* It is assume that absolute_path does not contain control characters nor -* spaces, see audit_string_contains_control(). +* It is assumed that absolute_path does not contain control +* characters nor spaces, see audit_string_contains_control(). */ absolute_path =realpath (path,NULL );if (!absolute_path)
@@ -7500,4 +7506,2212 @@ TEST_F(audit_layout1, mount)EXPECT_EQ (1 , records.domain); } +static bool debug_quiet_tests; + +FIXTURE (audit_quiet_layout1) +{ +struct audit_filter audit_filter; +int audit_fd; +}; + +FIXTURE_SETUP (audit_quiet_layout1) +{ +prepare_layout (_metadata); +create_layout1 (_metadata); + +set_cap (_metadata,CAP_AUDIT_CONTROL ); + self->audit_fd =audit_init_with_exe_filter (&self->audit_filter); +EXPECT_LE (0 , self->audit_fd); +clear_cap (_metadata,CAP_AUDIT_CONTROL ); + +if (getenv ("DEBUG_QUIET_TESTS" )) + debug_quiet_tests = true; +} + +FIXTURE_TEARDOWN_PARENT (audit_quiet_layout1) +{ +remove_layout1 (_metadata); +cleanup_layout (_metadata); + +set_cap (_metadata,CAP_AUDIT_CONTROL ); +EXPECT_EQ (0 ,audit_cleanup (-1 ,NULL )); +clear_cap (_metadata,CAP_AUDIT_CONTROL ); +} + +struct a_rule { +const char *path; +__u64 access; +bool quiet; +}; + +struct a_layer { +__u64 handled_access_fs; +__u64 quiet_access_fs; +struct a_rule rules[6 ]; +__u64 restrict_flags; +}; + +struct a_target { +/* File/dir to try open. */ +const char *target; +/* Open mode (one of O_RDONLY, O_WRONLY, or O_RDWR). */ +int open_mode; +/* Should open succeed? */ +bool expect_open_success; +/* If open fails, whether to expect an audit log for read. */ +bool audit_read_blocked; +/* If open fails, whether to expect an audit log for write. */ +bool audit_write_blocked; +/* If ftruncate() is expected to be allowed. */ +bool expect_truncate_success; +/* If ftruncate fails, whether to expect an audit log. */ +bool audit_truncate; +/* +* If ioctl() is expected to be allowed (ioctl not attempted if +* neither this nor expect_ioctl_denied is set). +*/ +bool expect_ioctl_allowed; +/* If ioctl() is expected to be denied. */ +bool expect_ioctl_denied; +/* If ioctl fails, whether to expect an audit log. */ +bool audit_ioctl; +}; + +#define AUDIT_QUIET_MAX_TARGETS 10 + +FIXTURE_VARIANT (audit_quiet_layout1) +{ +struct a_layer layers[3 ]; +struct a_target targets[AUDIT_QUIET_MAX_TARGETS ]; +}; + +#define FS_R LANDLOCK_ACCESS_FS_READ_FILE +#define FS_W LANDLOCK_ACCESS_FS_WRITE_FILE +#define FS_TRUNC LANDLOCK_ACCESS_FS_TRUNCATE +#define FS_IOCTL LANDLOCK_ACCESS_FS_IOCTL_DEV + +static int sprint_access_bits (char *buf,size_t buflen,__u64 access) +{ +size_t offset =0 ; + +if (buflen <strlen ("rwti make_reg remove_file refer" ) +1 ) +abort (); + + buf[0 ] ='\0' ; +if (access &FS_R ) + offset +=snprintf (buf + offset, buflen - offset,"r" ); +if (access &FS_W ) + offset +=snprintf (buf + offset, buflen - offset,"w" ); +if (access &FS_TRUNC ) + offset +=snprintf (buf + offset, buflen - offset,"t" ); +if (access &FS_IOCTL ) + offset +=snprintf (buf + offset, buflen - offset,"i" ); +if (access &LANDLOCK_ACCESS_FS_MAKE_REG ) + offset +=snprintf (buf + offset, buflen - offset,",make_reg" ); +if (access &LANDLOCK_ACCESS_FS_REMOVE_FILE ) + offset += +snprintf (buf + offset, buflen - offset,",remove_file" ); +if (access &LANDLOCK_ACCESS_FS_REFER ) + offset +=snprintf (buf + offset, buflen - offset,",refer" ); + +if (buf[0 ] ==',' ) { + offset--; +memmove (buf, buf +1 , offset); + buf[offset] ='\0' ; + } + +return offset; +} + +static int apply_a_layer (struct __test_metadata *const _metadata, +const struct a_layer *l) +{ +struct landlock_ruleset_attr rs_attr = { + .handled_access_fs = l->handled_access_fs, + .quiet_access_fs = l->quiet_access_fs, + }; +int rs_fd; +int i; +const struct a_rule *r; +char handled_access_s[33 ], quiet_access_s[33 ], rule_access_s[33 ]; + +if (!l->handled_access_fs) +return 0 ; + + rs_fd =landlock_create_ruleset (&rs_attr,sizeof (rs_attr),0 ); +ASSERT_LE (0 , rs_fd); + +for (i =0 ; i <ARRAY_SIZE (l->rules); i++) { + r = &l->rules[i]; +if (!r->path) +continue ; + +add_path_beneath (_metadata, rs_fd, r->access, r->path, + r->quiet); + } + +ASSERT_EQ (0 ,prctl (PR_SET_NO_NEW_PRIVS ,1 ,0 ,0 ,0 )); + ASSERT_EQ(0, landlock_restrict_self(rs_fd, l->restrict_flags)) + { +TH_LOG ("Failed to enforce ruleset: %s" ,strerror (errno)); + } +ASSERT_EQ (0 ,close (rs_fd)); + +if (debug_quiet_tests) { +sprint_access_bits (handled_access_s,sizeof (handled_access_s), + l->handled_access_fs); +sprint_access_bits (quiet_access_s,sizeof (quiet_access_s), + l->quiet_access_fs); +TH_LOG ("applied layer: handled=%s quiet=%s restrict_flags=0x%llx" , + handled_access_s, quiet_access_s, + (unsigned long long )l->restrict_flags); +for (i =0 ; i <ARRAY_SIZE (l->rules); i++) { + r = &l->rules[i]; +if (!r->path) +continue ; + +sprint_access_bits (rule_access_s,sizeof (rule_access_s), + r->access); +TH_LOG (" rule[%d]: path=%s access=%s quiet=%d" , i, + r->path, rule_access_s, r->quiet); + } + } +return 0 ; +} + +void audit_quiet_layout1_test_body (struct __test_metadata *const _metadata, +FIXTURE_DATA (audit_quiet_layout1) * self, +const struct a_target *targets) +{ +struct audit_records records = {}; +int i; +const struct a_target *target; +int fd =-1 ; +int open_mode; +int ret; +bool expect_audit; +const char *blocker; + +for (i =0 ; i <AUDIT_QUIET_MAX_TARGETS ; i++) { + target = &targets[i]; +if (!target->target) +continue ; + + open_mode = target->open_mode & (O_RDONLY |O_WRONLY |O_RDWR ); + +EXPECT_TRUE (open_mode ==O_RDONLY || open_mode ==O_WRONLY || + open_mode ==O_RDWR ); + +if (target->expect_open_success) { +EXPECT_FALSE (target->audit_read_blocked); +EXPECT_FALSE (target->audit_write_blocked); + } +if (target->expect_truncate_success) +EXPECT_TRUE (target->expect_open_success && + !target->audit_truncate); + +if (debug_quiet_tests) +TH_LOG ("Try open \"%s\" with %s%s" , target->target, + open_mode !=O_WRONLY ?"r" :"" , + open_mode !=O_RDONLY ?"w" :"" ); + + fd =openat (AT_FDCWD , target->target, open_mode |O_CLOEXEC ); +if (target->expect_open_success) { + ASSERT_LE(0, fd) + { +TH_LOG ("Failed to open \"%s\": %s" , + target->target,strerror (errno)); + }; + }else { +ASSERT_EQ (-1 , fd); +ASSERT_EQ (EACCES , errno); + } + + expect_audit = true; + +if (target->audit_read_blocked && target->audit_write_blocked) + blocker ="fs\\.write_file,fs\\.read_file" ; +else if (target->audit_read_blocked) + blocker ="fs\\.read_file" ; +else if (target->audit_write_blocked) + blocker ="fs\\.write_file" ; +else + expect_audit = false; + +if (expect_audit) +ASSERT_EQ (0 ,matches_log_fs (_metadata, self->audit_fd, + blocker, target->target)); + +/* +* Check that we see no (other) logs. +* +* We explicitly do not check records.domain here because sometimes, a +* domain deallocation log from a previous test (or even a previous +* run of the test binary when running in a loop) might run over and +* show up here. Since this is not a test about domain alloc/dealloc +* messages, we ignore them. +*/ +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); + +if (target->expect_open_success && fd >=0 ) { +if (debug_quiet_tests) +TH_LOG ("Try ftruncate \"%s\"" , target->target); + + ret =ftruncate (fd,0 ); +if (target->expect_truncate_success) { +ASSERT_EQ (0 , ret); + }else { +ASSERT_EQ (-1 , ret); +if (open_mode !=O_RDONLY ) +ASSERT_EQ (EACCES , errno); + } + +if (target->audit_truncate) +ASSERT_EQ (0 ,matches_log_fs (_metadata, + self->audit_fd, +"fs\\.truncate" , + target->target)); + +if (target->expect_ioctl_allowed || target->expect_ioctl_denied) { +if (debug_quiet_tests) +TH_LOG ("Try ioctl FIONREAD on \"%s\"" , + target->target); + + ret =ioctl_error (_metadata, fd,FIONREAD ); +if (target->expect_ioctl_allowed) { +ASSERT_NE (EACCES , ret); + }else { +ASSERT_EQ (EACCES , ret); + } + } + +if (target->audit_ioctl) +ASSERT_EQ (0 , +matches_log_fs_extra ( + _metadata, self->audit_fd, +"fs\\.ioctl_dev" , + target->target, +" ioctlcmd=0x541b\\+" )); + +/* No other logs. records.domain not checked per reasoning above. */ +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +ASSERT_EQ (0 ,close (fd)); + } + } +} + +TEST_F(audit_quiet_layout1, base) +{ +int i; + +for (i =0 ; i <ARRAY_SIZE (variant->layers); i++) +ASSERT_EQ (0 ,apply_a_layer (_metadata, &variant->layers[i])); + +audit_quiet_layout1_test_body (_metadata, self, variant->targets); +} + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_simple) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_R, + .rules = { + { .path = dir_s1d1, .access = 0, .quiet = true }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + /* Not covered by quiet */ + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + /* Access not quieted */ + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .audit_write_blocked = true, + }, + /* + * Quiet flag only takes effect if all blocked access bits are + * quieted, otherwise audit log emitted as normal (with all blockers) + */ + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_allow_read) { + .layers = { + { + .handled_access_fs =FS_R |FS_W |FS_TRUNC , + .quiet_access_fs = FS_W, + .rules = { + { .path = dir_s1d1, .access = FS_R, .quiet = true }, + /* Quiet flags inherit down and is not overridden */ + { .path = file1_s1d1, .access = FS_R, .quiet = false }, + { .path = file1_s2d3, .access = 0, .quiet = true }, + }, + }, + }, + .targets = { + /* Read ok */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + .expect_open_success = true, + }, + /* Write quieted */ + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + }, + /* Read allowed, write quieted so no audit */ + { + .target = file1_s1d1, + .open_mode = O_RDWR, + }, + /* Not covered by quiet */ + { + .target = file1_s2d2, + .open_mode = O_WRONLY, + .audit_write_blocked = true, + }, + { + .target = file1_s2d2, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + /* Single file quiet */ + { + .target = file1_s2d3, + .open_mode = O_WRONLY, + }, + /* Wrong file */ + { + .target = file2_s2d3, + .open_mode = O_WRONLY, + .audit_write_blocked = true, + }, + /* Access not quieted */ + { + .target = file1_s2d3, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + /* Some access not quieted */ + { + .target = file1_s2d3, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_allow_write) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_R, + .rules = { + { .path = dir_s1d1, .access = FS_W, .quiet = true }, + }, + }, + }, + .targets = { + /* Read quieted */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + /* Truncate not quieted */ + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + /* Not covered by quiet */ + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + /* Write allowed, read quieted so no audit */ + { + .target = file1_s1d1, + .open_mode =O_RDWR , + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, allow_write_quiet_trunc) { + .layers = { + { + .handled_access_fs =FS_R |FS_W |FS_TRUNC , + .quiet_access_fs = FS_TRUNC, + .rules = { + { .path = dir_s1d1, .access = FS_W, .quiet = true }, + { .path = dir_s2d1, .access = FS_W, .quiet = false }, + }, + }, + }, + .targets = { + /* Read not allowed and not quieted */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + /* Truncate quieted */ + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + }, + /* Not covered by quiet (truncate) */ + { + .target = file1_s2d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + /* Not covered by quiet (read/write) */ + { + .target = file1_s3d1, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, allow_rw_quiet_trunc) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_TRUNC, + .rules = { + { .path = dir_s1d1, .access = FS_R | FS_W, .quiet = true }, + { .path = dir_s2d1, .access = FS_R | FS_W, .quiet = false }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .expect_open_success = true, + }, + { + .target = file1_s2d1, + .open_mode = O_RDWR, + .expect_open_success = true, + .audit_truncate = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_all) { + .layers = { + { + .handled_access_fs =FS_R |FS_W |FS_TRUNC |FS_IOCTL , + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { .path = dir_s1d1, .access = 0, .quiet = true }, + { .path = file1_s2d1, .access = FS_R | FS_W, .quiet = true }, + { .path = file1_s2d3, .access = 0, .quiet = true }, + { .path = dir_s3d1, .access = FS_W, .quiet = false }, + { .path = "/dev/zero", .access = FS_R, .quiet = false }, + { .path = "/dev/null", .access = FS_R, .quiet = true }, + }, + }, + }, + .targets = { + /* No logs */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + }, + { + .target = file1_s1d1, + .open_mode = O_RDWR, + }, + /* Truncate quieted - no log */ + { + .target = file1_s2d1, + .open_mode = O_RDWR, + .expect_open_success = true, + }, + /* Truncate not covered by quiet */ + { + .target = file1_s3d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + /* Not covered by quiet */ + { + .target = file1_s3d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + /* Single file quiet */ + { + .target = file1_s2d3, + .open_mode = O_RDWR, + }, + /* Wrong file */ + { + .target = file2_s2d3, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + /* Ioctl quieted */ + { + .target ="/dev/null" , + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + /* Ioctl not quieted */ + { + .target ="/dev/zero" , + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + .audit_ioctl = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_across_mountpoint) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_R, + .rules = { + { .path = dir_s3d1, .access = 0, .quiet = true }, + }, + }, + }, + .targets = { + { + .target = file1_s3d3, + .open_mode = O_RDONLY, + }, + /* Not covered by quiet */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + /* Access not quieted */ + { + .target = file1_s3d3, + .open_mode = O_WRONLY, + .audit_write_blocked = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, allow_all_quiet) { + .layers = { + { + .handled_access_fs =FS_R |FS_W |FS_TRUNC |FS_IOCTL , + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = true + }, + { + .path ="/dev/null" , + .access = FS_R | FS_W | FS_IOCTL, + .quiet = true + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .expect_open_success = true, + .expect_truncate_success = true, + }, + { + .target ="/dev/null" , + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_allowed = true, + }, + }, +}; + +/* +* With LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, it doesn't matter what +* the quiet flags below the layer says +*/ +FIXTURE_VARIANT_ADD(audit_quiet_layout1, subdomains_off) { + .layers = { + { + .handled_access_fs = FS_R, + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, + .rules = { + { .path = "/", .access = FS_R, .quiet = false }, + } + }, + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R, + .rules = { + { .path = dir_s1d1, .access = 0, .quiet = true }, + { .path = file1_s2d2, .access = FS_R | FS_W, .quiet = true }, + { .path = file1_s2d3, .access = FS_R | FS_W, .quiet = false }, + { .path = "/dev/null", .access = FS_R | FS_W, .quiet = true }, + { .path = "/dev/zero", .access = FS_R | FS_W, .quiet = false }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + }, + { + .target = file1_s2d1, + .open_mode = O_RDWR, + }, + { + .target = file1_s2d2, + .open_mode = O_RDWR, + .expect_open_success = true, +/* No audit_truncate */ + }, + { + .target = file1_s2d3, + .open_mode = O_RDWR, + .expect_open_success = true, +/* No audit_truncate */ + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, +/* No audit_ioctl */ + }, + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + /* No audit_ioctl */ + }, + }, +}; + +/* +* With LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF, it doesn't matter what +* the quiet flags on the layer says +*/ +FIXTURE_VARIANT_ADD(audit_quiet_layout1, same_exec_off) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R, + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF, + .rules = { + { .path = dir_s1d1, .access = 0, .quiet = true }, + { .path = file1_s2d2, .access = FS_R | FS_W, .quiet = true }, + { .path = file1_s2d3, .access = FS_R | FS_W, .quiet = false }, + { .path = "/dev/null", .access = FS_R | FS_W, .quiet = true }, + { .path = "/dev/zero", .access = FS_R | FS_W, .quiet = false }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + }, + { + .target = file1_s2d1, + .open_mode = O_RDWR, + }, + { + .target = file1_s2d2, + .open_mode = O_RDWR, + .expect_open_success = true, +/* No audit_truncate */ + }, + { + .target = file1_s2d3, + .open_mode = O_RDWR, + .expect_open_success = true, +/* No audit_truncate */ + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, +/* No audit_ioctl */ + }, + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + /* No audit_ioctl */ + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_1) { +/* Here, rules that deny access is always quiet. */ + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_W, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path = "/dev/null", + .access = FS_R, + .quiet = true, + }, + { + .path = "/dev/zero", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + }, + }, + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = FS_W, + .quiet = true, + }, + { + .path = "/dev/null", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + { + .path = "/dev/zero", + .access = FS_R, + .quiet = true, + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + }, + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + }, + { + .target = file1_s2d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_2) { +/* Here, rules that deny access is never quiet. */ + .layers = { + { + .handled_access_fs =FS_R |FS_W |FS_TRUNC |FS_IOCTL , + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_W, + .quiet = false + }, + { + .path = dir_s2d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = true + }, + { + .path ="/dev/null" , + .access = FS_R, + .quiet = false + }, + { + .path ="/dev/zero" , + .access = FS_R | FS_W | FS_IOCTL, + .quiet = true + }, + }, + }, + { + .handled_access_fs =FS_R |FS_W |FS_TRUNC |FS_IOCTL , + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = true + }, + { + .path = dir_s2d1, + .access = FS_W, + .quiet = false + }, + { + .path ="/dev/null" , + .access = FS_R | FS_W | FS_IOCTL, + .quiet = true + }, + { + .path ="/dev/zero" , + .access = FS_R, + .quiet = false + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + { + .target = file1_s2d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + { + .target ="/dev/null" , + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + .audit_ioctl = true, + }, + { + .target ="/dev/zero" , + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + .audit_ioctl = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_3) { +/* This time only the second layer quiets things. */ + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_W, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path = "/dev/null", + .access = FS_R, + .quiet = false, + }, + { + .path = "/dev/zero", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + }, + }, + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = FS_W, + .quiet = true, + }, + { + .path = "/dev/null", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + { + .path = "/dev/zero", + .access = FS_R, + .quiet = true, + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + }, + { + .target = file1_s2d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + .audit_ioctl = true, + }, + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_different_quiet_access) { +/* Here, rules that deny access is always quiet. */ + .layers = { + { + .handled_access_fs =FS_R |FS_W |FS_TRUNC |FS_IOCTL , + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_W, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path ="/dev/null" , + .access = FS_R, + .quiet = true, + }, + { + .path ="/dev/zero" , + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + }, + }, + { + .handled_access_fs =FS_R |FS_W |FS_TRUNC |FS_IOCTL , + .quiet_access_fs = FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = FS_W, + .quiet = true, + }, + { + .path ="/dev/null" , + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + { + .path ="/dev/zero" , + .access = FS_R, + .quiet = true, + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + }, + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + { + .target = file1_s2d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + { + .target ="/dev/null" , + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + { + .target ="/dev/zero" , + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, without_quiet_then_with_quiet) { + .layers = { + { + .handled_access_fs = FS_R | FS_W, + .quiet_access_fs = FS_R, + .rules = { + { .path = dir_s1d1, .access = FS_W, .quiet = false }, + { .path = dir_s1d1, .access = 0, .quiet = true }, + }, + }, + }, + .targets = { + /* Read denied and quieted */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + /* Write ok */ + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .expect_truncate_success = true, + }, + /* Write ok, read denied and quieted */ + { + .target = file1_s1d1, + .open_mode = O_RDWR, + }, + /* Not covered by quiet */ + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + }, +}; + +/* +* The following TEST_F extend the above test cases to test more layers, +* with the inserted layers having varying configurations. +*/ + +/* Extra allow all layers, quiet or not, does not change any behaviour. */ +TEST_F(audit_quiet_layout1, allow_all_layer) +{ +struct a_layer allow_all_layer = { + .handled_access_fs =FS_R |FS_W |FS_TRUNC |FS_IOCTL , + .quiet_access_fs =0 , + .rules = { + { + .path ="/" , + .access =FS_R |FS_W |FS_TRUNC |FS_IOCTL , + .quiet = false, + }, + }, + }; +int i; + +ASSERT_EQ (0 ,apply_a_layer (_metadata, &allow_all_layer)); +for (i =0 ; i <ARRAY_SIZE (variant->layers); i++) +ASSERT_EQ (0 ,apply_a_layer (_metadata, &variant->layers[i])); + +audit_quiet_layout1_test_body (_metadata, self, variant->targets); + +ASSERT_EQ (0 ,apply_a_layer (_metadata, &allow_all_layer)); + +audit_quiet_layout1_test_body (_metadata, self, variant->targets); + +/* +* SELF_LOG flags or quiet bits from inner allowing layers should not +* affect behaviour. +*/ + allow_all_layer.quiet_access_fs =FS_R |FS_W |FS_TRUNC |FS_IOCTL ; + allow_all_layer.rules[0 ].quiet = true; +/* +* Note: this only works because we're not checking counts of domain +* alloc/dealloc logs +*/ + allow_all_layer.restrict_flags = +LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF | +LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF ; +ASSERT_EQ (0 ,apply_a_layer (_metadata, &allow_all_layer)); + +audit_quiet_layout1_test_body (_metadata, self, variant->targets); +} + +/* +* Add useless outer layers until we reach the layer limit. Should not +* change anything. +*/ +TEST_F(audit_quiet_layout1, many_outer_layers) +{ +struct a_layer useless_layer = { + .handled_access_fs =FS_R |FS_W |FS_TRUNC , + .quiet_access_fs =FS_R |FS_W |FS_TRUNC , + .rules = { + { .path ="/" , .access =FS_R |FS_W |FS_TRUNC , .quiet = true }, + }, + }; +int i; + +for (i =0 ; i <ARRAY_SIZE (variant->layers); i++) { +if (variant->layers[i].handled_access_fs ==0 ) +break ; + } + +for (; i <LANDLOCK_MAX_NUM_LAYERS ; i++) +ASSERT_EQ (0 ,apply_a_layer (_metadata, &useless_layer)); + +for (i =0 ; i <ARRAY_SIZE (variant->layers); i++) +ASSERT_EQ (0 ,apply_a_layer (_metadata, &variant->layers[i])); + +audit_quiet_layout1_test_body (_metadata, self, variant->targets); +} + +/* +* An inner layer that denies and quiets everything should result in no +* logs. +*/ +TEST_F(audit_quiet_layout1, deny_all_quiet_layer) +{ +struct a_layer deny_all_layer = { + .handled_access_fs =FS_R |FS_W |FS_TRUNC |FS_IOCTL , + .quiet_access_fs =FS_R |FS_W |FS_TRUNC |FS_IOCTL , + .rules = { + { .path ="/" , .access =0 , .quiet = true }, + }, + }; +int i; +FIXTURE_VARIANT (audit_quiet_layout1) variant_2 = {}; + +/* Any open should fail with no logs. */ +for (i =0 ; i <ARRAY_SIZE (variant->targets); i++) { +const struct a_target *target = &variant->targets[i]; + + variant_2.targets[i] = (struct a_target ){ + .target = target->target, + .open_mode = target->open_mode, +/* We denied everything, open should always fail. */ + .expect_open_success = false, + }; + } + +for (i =0 ; i <ARRAY_SIZE (variant->layers); i++) +ASSERT_EQ (0 ,apply_a_layer (_metadata, &variant->layers[i])); +ASSERT_EQ (0 ,apply_a_layer (_metadata, &deny_all_layer)); + +audit_quiet_layout1_test_body (_metadata, self, variant_2.targets); +} + +/* +* An inner layer that denies everything without quiet should produce logs +* for all access. +*/ +TEST_F(audit_quiet_layout1, deny_all_layer) +{ +struct a_layer deny_all_layer = { + .handled_access_fs =FS_R |FS_W , + .quiet_access_fs =FS_R |FS_W , + }; +int i; +FIXTURE_VARIANT (audit_quiet_layout1) variant_2 = {}; +bool test_has_subdomains_off = false; + +for (i =0 ; i <ARRAY_SIZE (variant->layers); i++) { +if (variant->layers[i].restrict_flags & +LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF ) { + test_has_subdomains_off = true; +break ; + } + } + +for (i =0 ; i <ARRAY_SIZE (variant->targets); i++) { +const struct a_target *target = &variant->targets[i]; + + variant_2.targets[i] = (struct a_target ){ + .target = target->target, + .open_mode = target->open_mode, + +/* We denied everything, open should always fail. */ + .expect_open_success = false, +/* Audit should always happen as long as open request contains read. */ + .audit_read_blocked = !test_has_subdomains_off && + target->open_mode !=O_WRONLY , +/* Audit should always happen as long as open request contains write. */ + .audit_write_blocked = !test_has_subdomains_off && + target->open_mode !=O_RDONLY , + }; + } + +for (i =0 ; i <ARRAY_SIZE (variant->layers); i++) +ASSERT_EQ (0 ,apply_a_layer (_metadata, &variant->layers[i])); +ASSERT_EQ (0 ,apply_a_layer (_metadata, &deny_all_layer)); + +audit_quiet_layout1_test_body (_metadata, self, variant_2.targets); +} + +/* Uses layout1_bind hierarchy */ +FIXTURE (audit_quiet_rename) +{ +struct audit_filter audit_filter; +int audit_fd; +}; + +FIXTURE_SETUP (audit_quiet_rename) +{ +prepare_layout (_metadata); +create_layout1 (_metadata); + +set_cap (_metadata,CAP_SYS_ADMIN ); +ASSERT_EQ (0 ,mount (dir_s1d2, dir_s2d2,NULL ,MS_BIND ,NULL )); +clear_cap (_metadata,CAP_SYS_ADMIN ); + +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 ); + +if (getenv ("DEBUG_QUIET_TESTS" )) + debug_quiet_tests = true; +} + +FIXTURE_TEARDOWN_PARENT (audit_quiet_rename) +{ +remove_layout1 (_metadata); +cleanup_layout (_metadata); + +/* umount(dir_s2d2)) is handled by namespace lifetime. */ + +remove_path (file1_s4d1); +remove_path (file2_s4d1); + +set_cap (_metadata,CAP_AUDIT_CONTROL ); +EXPECT_EQ (0 ,audit_cleanup (-1 ,NULL )); +clear_cap (_metadata,CAP_AUDIT_CONTROL ); +} + +static void simple_quiet_rename (struct __test_metadata *const _metadata, +FIXTURE_DATA (audit_quiet_rename) *const self, +__u64 handled_access,__u64 quiet_access, +bool source_allow,bool dest_allow, +bool source_quiet,bool dest_quiet, +const char *source_blockers, +const char *dest_blockers) +{ +/* We will move file1_s1d1 to file1_s2d1 */ +struct a_layer layer = { + .handled_access_fs = handled_access, + .quiet_access_fs = quiet_access, + .rules = { + { + .path = dir_s1d1, + .access = source_allow ? handled_access :0 , + .quiet = source_quiet, + }, + { + .path = dir_s2d1, + .access = dest_allow ? handled_access :0 , + .quiet = dest_quiet, + }, + }, + }; +struct audit_records records = {}; +int ret, err; + +/* Skip landlock_add_rule for useless rules. */ +if (!source_allow && !source_quiet) + layer.rules[0 ].path =NULL ; +if (!dest_allow && !dest_quiet) + layer.rules[1 ].path =NULL ; + +EXPECT_EQ (0 ,unlink (file1_s2d1)); +EXPECT_EQ (0 ,apply_a_layer (_metadata, &layer)); + +if (debug_quiet_tests) +TH_LOG ("Try renameat \"%s\" to \"%s\"" , file1_s1d1, file1_s2d1); + ret =renameat (AT_FDCWD , file1_s1d1,AT_FDCWD , file1_s2d1); + err = errno; +if (ret !=0 && debug_quiet_tests) { +TH_LOG ("renameat error: %s" , err ==EXDEV ?"EXDEV" : + err ==EACCES ?"EACCES" : +strerror (err)); + } +if (source_allow && dest_allow) { +ASSERT_EQ (0 , ret); + }else { +ASSERT_EQ (-1 , ret); +if (handled_access & (LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE )) { +ASSERT_EQ (EACCES , err); + }else { +ASSERT_EQ (EXDEV , err); + } + +if (source_blockers) +ASSERT_EQ (0 ,matches_log_fs (_metadata, self->audit_fd, + source_blockers, dir_s1d1)); +if (dest_blockers) +ASSERT_EQ (0 ,matches_log_fs (_metadata, self->audit_fd, + dest_blockers, dir_s2d1)); + } +/* +* No other logs. records.domain not checked per reasoning in +* audit_quiet_layout1_test_body. +*/ +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +} + +TEST_F(audit_quiet_rename, rename_ok) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; + +simple_quiet_rename (_metadata, self, access, access, true, true, false, + false,NULL ,NULL ); +} + +TEST_F(audit_quiet_rename, no_quiet) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; + +simple_quiet_rename (_metadata, self, access, access, false, false, + false, false,"fs\\.remove_file,fs\\.refer" , +"fs\\.make_reg,fs\\.refer" ); +} + +TEST_F(audit_quiet_rename, quiet) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; + +simple_quiet_rename (_metadata, self, access, access, false, false, true, + true,NULL ,NULL ); +} + +TEST_F(audit_quiet_rename, source_no_quiet_dest_quiet) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; + +simple_quiet_rename (_metadata, self, access, access, false, false, + false, true,"fs\\.remove_file,fs\\.refer" ,NULL ); +} + +TEST_F(audit_quiet_rename, source_quiet_dest_no_quiet) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; + +simple_quiet_rename (_metadata, self, access, access, false, false, true, + false,NULL ,"fs\\.make_reg,fs\\.refer" ); +} + +TEST_F(audit_quiet_rename, only_quiet_refer) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; + +simple_quiet_rename (_metadata, self, access,LANDLOCK_ACCESS_FS_REFER , + false, false, true, true, +"fs\\.remove_file,fs\\.refer" , +"fs\\.make_reg,fs\\.refer" ); +} + +TEST_F(audit_quiet_rename, source_allow_dest_quiet) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; + +simple_quiet_rename (_metadata, self, access, access, true, false, false, + true,NULL ,NULL ); +} + +TEST_F(audit_quiet_rename, source_quiet_dest_allow) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; + +simple_quiet_rename (_metadata, self, access, access, false, true, true, + false,NULL ,NULL ); +} + +TEST_F(audit_quiet_rename, handle_all_deny_quiet_refer) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; +struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs =LANDLOCK_ACCESS_FS_REFER , + .rules = { + { + .path = dir_s1d1, + .access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE , + .quiet = true, + }, + { + .path = dir_s2d1, + .access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE , + .quiet = true, + }, + }, + }; +struct audit_records records = {}; + +EXPECT_EQ (0 ,unlink (file1_s2d1)); +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer)); + +ASSERT_EQ (-1 ,renameat (AT_FDCWD , file1_s1d1,AT_FDCWD , file1_s2d1)); +ASSERT_EQ (EXDEV , errno); + +/* No logs */ +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +} + +TEST_F(audit_quiet_rename, handle_all_deny_not_quiet_refer) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; +struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs =0 , + .rules = { + { + .path = dir_s1d1, + .access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE , + .quiet = false, + }, + { + .path = dir_s2d1, + .access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE , + .quiet = false, + }, + }, + }; +struct audit_records records = {}; + +EXPECT_EQ (0 ,unlink (file1_s2d1)); +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer)); + +ASSERT_EQ (-1 ,renameat (AT_FDCWD , file1_s1d1,AT_FDCWD , file1_s2d1)); +ASSERT_EQ (EXDEV , errno); + +ASSERT_EQ (0 ,matches_log_fs (_metadata, self->audit_fd,"fs\\.refer" , + dir_s1d1)); +ASSERT_EQ (0 ,matches_log_fs (_metadata, self->audit_fd,"fs\\.refer" , + dir_s2d1)); + +/* No other logs */ +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +} + +TEST_F(audit_quiet_rename, handle_all_deny_refer_quiet_source_not_quiet_dest) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; +struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs =LANDLOCK_ACCESS_FS_REFER , + .rules = { + { + .path = dir_s1d1, + .access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE , + .quiet = true, + }, + { + .path = dir_s2d1, + .access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE , + .quiet = false, + }, + }, + }; +struct audit_records records = {}; + +EXPECT_EQ (0 ,unlink (file1_s2d1)); +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer)); + +ASSERT_EQ (-1 ,renameat (AT_FDCWD , file1_s1d1,AT_FDCWD , file1_s2d1)); +ASSERT_EQ (EXDEV , errno); + +ASSERT_EQ (0 ,matches_log_fs (_metadata, self->audit_fd,"fs\\.refer" , + dir_s2d1)); + +/* No other logs */ +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +} + +TEST_F(audit_quiet_rename, quiet_same_dir) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; +struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access =0 , + .quiet = true, + }, + }, + }; +struct audit_records records = {}; + +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer)); + +ASSERT_EQ (-1 ,renameat (AT_FDCWD , file1_s1d1,AT_FDCWD , file2_s1d1)); +ASSERT_EQ (EACCES , errno); + +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +} + +TEST_F(audit_quiet_rename, quiet_flag_on_file_ignored) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; +struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = file1_s1d1, + .access =0 , + .quiet = true, + }, + { + .path = file1_s2d1, + .access =0 , + .quiet = true, + }, + }, + }; +struct audit_records records = {}; + +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer)); + +ASSERT_EQ (-1 ,renameat (AT_FDCWD , file1_s1d1,AT_FDCWD , file1_s2d1)); +ASSERT_EQ (EACCES , errno); + +ASSERT_EQ (0 ,matches_log_fs (_metadata, self->audit_fd, +"fs\\.remove_file,fs\\.refer" , dir_s1d1)); +/* We didn't unlink destination file */ +ASSERT_EQ (0 ,matches_log_fs (_metadata, self->audit_fd, +"fs\\.remove_file,fs\\.make_reg,fs\\.refer" , dir_s2d1)); + +/* No other logs */ +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +} + +TEST_F(audit_quiet_rename, quiet_flag_on_file_ignored_same_dir) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; +struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = file1_s1d1, + .access =0 , + .quiet = true, + }, + { + .path = file2_s1d1, + .access =0 , + .quiet = true, + }, + }, + }; +struct audit_records records = {}; + +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer)); + +ASSERT_EQ (-1 ,renameat (AT_FDCWD , file1_s1d1,AT_FDCWD , file2_s1d1)); +ASSERT_EQ (EACCES , errno); + +ASSERT_EQ (0 , +matches_log_fs (_metadata, self->audit_fd, +"fs\\.remove_file,fs\\.make_reg" , dir_s1d1)); + +/* No other logs */ +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +} + +TEST_F(audit_quiet_rename, two_layers_different_quiet1) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; +struct a_layer layer1 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = access, + .quiet = false, + }, + { + .path = dir_s2d1, + .access =0 , + .quiet = true, + }, + }, + }; +struct a_layer layer2 = { + .handled_access_fs = access, + .quiet_access_fs =LANDLOCK_ACCESS_FS_REFER , + .rules = { + { + .path = dir_s1d1, + .access =0 , + .quiet = true, + }, + { + .path = dir_s2d1, + .access = access, + .quiet = false, + }, + }, + }; +struct audit_records records = {}; + +EXPECT_EQ (0 ,unlink (file1_s2d1)); + +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer1)); +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer2)); + +ASSERT_EQ (-1 ,renameat (AT_FDCWD , file1_s1d1,AT_FDCWD , file1_s2d1)); +ASSERT_EQ (EACCES , errno); + +/* +* The youngest denial will be layer 2. Refer is quieted but we are +* also missing remove_file on source. +*/ +ASSERT_EQ (0 ,matches_log_fs (_metadata, self->audit_fd, +"fs\\.remove_file,fs\\.refer" , dir_s1d1)); +/* No other logs */ +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +} + +TEST_F(audit_quiet_rename, two_layers_different_quiet2) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; +struct a_layer layer1 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = access, + .quiet = false, + }, + { + .path = dir_s2d1, + .access =0 , + .quiet = true, + }, + }, + }; +struct a_layer layer2 = { + .handled_access_fs =LANDLOCK_ACCESS_FS_REFER , + .quiet_access_fs =LANDLOCK_ACCESS_FS_REFER , + .rules = { + { + .path = dir_s1d1, + .access =0 , + .quiet = true, + }, + { + .path = dir_s2d1, + .access =LANDLOCK_ACCESS_FS_REFER , + .quiet = false, + }, + }, + }; +struct audit_records records = {}; + +EXPECT_EQ (0 ,unlink (file1_s2d1)); + +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer1)); +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer2)); + +ASSERT_EQ (-1 ,renameat (AT_FDCWD , file1_s1d1,AT_FDCWD , file1_s2d1)); +ASSERT_EQ (EACCES , errno); + +/* +* The youngest denial will be layer 2, but refer is quieted (and that +* layer does not handle any other accesses). +*/ +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +} + +TEST_F(audit_quiet_rename, two_layers_different_quiet3) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; +struct a_layer layer1 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = access, + .quiet = false, + }, + { + .path = dir_s2d1, + .access =0 , + .quiet = true, + }, + }, + }; +struct a_layer layer2 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access =0 , + .quiet = true, + }, + { + .path = dir_s2d1, + .access = access, + .quiet = false, + }, + }, + }; +struct audit_records records = {}; + +EXPECT_EQ (0 ,unlink (file1_s2d1)); + +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer1)); +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer2)); + +ASSERT_EQ (-1 ,renameat (AT_FDCWD , file1_s1d1,AT_FDCWD , file1_s2d1)); +ASSERT_EQ (EACCES , errno); + +/* +* The youngest denial will be layer 2, in which everything is +* quieted. +*/ +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +} + +TEST_F(audit_quiet_rename, first_layer_quiet_deny_all_second_layer_not_quiet_deny_all) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; +struct a_layer layer1 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access =0 , + .quiet = true, + }, + { + .path = dir_s2d1, + .access =0 , + .quiet = true, + }, + }, + }; +struct a_layer layer2 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = {}, + }; +struct audit_records records = {}; + +EXPECT_EQ (0 ,unlink (file1_s2d1)); + +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer1)); +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer2)); + +ASSERT_EQ (-1 ,renameat (AT_FDCWD , file1_s1d1,AT_FDCWD , file1_s2d1)); +ASSERT_EQ (EACCES , errno); + +ASSERT_EQ (0 ,matches_log_fs (_metadata, self->audit_fd, +"fs\\.remove_file,fs\\.refer" , dir_s1d1)); +ASSERT_EQ (0 ,matches_log_fs (_metadata, self->audit_fd, +"fs\\.make_reg,fs\\.refer" , dir_s2d1)); +/* No other logs. */ +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +} + +TEST_F(audit_quiet_rename, first_layer_quiet_deny_all_second_layer_dest_not_quiet) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; +struct a_layer layer1 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access =0 , + .quiet = true, + }, + { + .path = dir_s2d1, + .access =0 , + .quiet = true, + }, + }, + }; +struct a_layer layer2 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access =0 , + .quiet = true, + }, + }, + }; +struct audit_records records = {}; + +EXPECT_EQ (0 ,unlink (file1_s2d1)); + +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer1)); +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer2)); + +ASSERT_EQ (-1 ,renameat (AT_FDCWD , file1_s1d1,AT_FDCWD , file1_s2d1)); +ASSERT_EQ (EACCES , errno); + +/* +* Source is quieted but destination is not. +*/ +ASSERT_EQ (0 ,matches_log_fs (_metadata, self->audit_fd, +"fs\\.make_reg,fs\\.refer" , dir_s2d1)); +/* No other logs. */ +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +} + +TEST_F(audit_quiet_rename, rename_xchg) +{ +struct a_layer layer = { + .handled_access_fs =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER , + .quiet_access_fs =LANDLOCK_ACCESS_FS_MAKE_REG , + .rules = { + { + .path = dir_s1d1, + .access =LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER , + .quiet = true, + }, + { + .path = dir_s2d1, + .access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER , + .quiet = false, + } + }, + }; +struct audit_records records = {}; + +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer)); + +ASSERT_EQ (-1 ,renameat2 (AT_FDCWD , file1_s1d1,AT_FDCWD , file1_s2d1, +RENAME_EXCHANGE )); +ASSERT_EQ (EACCES , errno); + +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +} + +TEST_F(audit_quiet_rename, quiet_on_parent_mount) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; +struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s2d1, + .access =0 , + .quiet = true, + }, + }, + }; +struct audit_records records = {}; + +EXPECT_EQ (0 ,unlink (file2_s1d3)); +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer)); + +ASSERT_EQ (-1 ,renameat (AT_FDCWD , bind_file1_s1d3,AT_FDCWD , bind_file2_s1d3)); +ASSERT_EQ (EACCES , errno); + +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +} + +TEST_F(audit_quiet_rename, quiet_behind_mountpoint_ignored) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; +struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access =0 , + .quiet = true, + }, + }, + }; +struct audit_records records = {}; + +EXPECT_EQ (0 ,unlink (file2_s1d3)); +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer)); + +ASSERT_EQ (-1 ,renameat (AT_FDCWD , bind_file1_s1d3,AT_FDCWD , bind_file2_s1d3)); +ASSERT_EQ (EACCES , errno); +ASSERT_EQ (0 , +matches_log_fs (_metadata, self->audit_fd, +"fs\\.remove_file,fs\\.make_reg" , bind_dir_s1d3)); + +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +} + +TEST_F(audit_quiet_rename, quiet_on_parent_mount_disconnected) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; +struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s2d1, + .access =0 , + .quiet = true, + }, + }, + }; +struct audit_records records = {}; +int bind_s1d3_fd; + +EXPECT_EQ (0 ,unlink (file2_s1d3)); + + bind_s1d3_fd =open (bind_dir_s1d3,O_PATH |O_DIRECTORY ); +ASSERT_GE (bind_s1d3_fd,0 ); + +/* Make s1d3 disconnected. */ +create_directory (_metadata, dir_s4d1); +ASSERT_EQ (0 ,renameat (AT_FDCWD , dir_s1d3,AT_FDCWD , dir_s4d2)); + +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer)); + +ASSERT_EQ (-1 ,renameat (bind_s1d3_fd, file1_name, bind_s1d3_fd, file2_name)); +ASSERT_EQ (EACCES , errno); + +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +} + +TEST_F(audit_quiet_rename, quiet_behind_mountpoint_ignored_disconnected) +{ +__u64 access =LANDLOCK_ACCESS_FS_MAKE_REG | +LANDLOCK_ACCESS_FS_REMOVE_FILE | +LANDLOCK_ACCESS_FS_REFER ; +struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s4d1, + .access =0 , + .quiet = true, + }, + }, + }; +struct audit_records records = {}; +int bind_s1d3_fd; + +EXPECT_EQ (0 ,unlink (file2_s1d3)); + + bind_s1d3_fd =open (bind_dir_s1d3,O_PATH |O_DIRECTORY ); +ASSERT_GE (bind_s1d3_fd,0 ); + +/* Make s1d3 disconnected. */ +create_directory (_metadata, dir_s4d1); +ASSERT_EQ (0 ,renameat (AT_FDCWD , dir_s1d3,AT_FDCWD , dir_s4d2)); + +ASSERT_EQ (0 ,apply_a_layer (_metadata, &layer)); + +ASSERT_EQ (-1 ,renameat (bind_s1d3_fd, file1_name, bind_s1d3_fd, file2_name)); +ASSERT_EQ (EACCES , errno); + +/* Disconnected paths are logged as "/". */ +ASSERT_EQ (0 ,matches_log_fs (_metadata, self->audit_fd, +"fs\\.remove_file,fs\\.make_reg" ,"/" )); + +audit_count_records (self->audit_fd, &records); +ASSERT_EQ (0 , records.access); +} +TEST_HARNESS_MAIN
Signature · 1 line
2.51.1
PATCH v3 8/8 selftests/landlock: add tests for quiet flag with net rules
In reply to Tingmao WangTests that:
- Quiet flag works on network rules
- Quiet flag applied to unrelated ports has no effect
- Denied access not in quiet_access_net is still logged
This is not as thorough as the fs tests, but given the shared logic it should be sufficient. There is also no "optional" access for network rules.
---
Changes since v2:
- New patch
tools/testing/selftests/landlock/net_test.c | 121 ++++++++++++++++++-- 1 file changed, 111 insertions(+), 10 deletions(-)
tools/testing/selftests/landlock/net_test.c +111 -10
@@ -1871,21 +1871,22 @@ TEST_F(port_specific, bind_connect_1023)static int matches_log_tcp (const int audit_fd,const char *const blockers,const char *const dir_addr,const char *const addr, -const char *const dir_port) +const char *const dir_port,const __u16 port) {static const char log_template[] =REGEX_LANDLOCK_PREFIX -" blockers=%s %s=%s %s=1024$" ; +" blockers=%s %s=%s %s=%u$" ;/* * Max strlen(blockers): 16 * Max strlen(dir_addr): 5 * Max strlen(addr): 12 * Max strlen(dir_port): 4 +* Max strlen(%d port): 5 */ -char log_match[sizeof (log_template) +37 ]; +char log_match[sizeof (log_template) +42 ];int log_match_len; log_match_len =snprintf (log_match,sizeof (log_match), log_template, - blockers, dir_addr, addr, dir_port); + blockers, dir_addr, addr, dir_port, port);if (log_match_len >sizeof (log_match))return -E2BIG ;
@@ -1895,7 +1896,8 @@ static int matches_log_tcp(const int audit_fd, const char *const blockers,FIXTURE (audit) { -struct service_fixture srv0; +/* srv1 has a rule with no access but quiet bit set, srv0 does not. */ +struct service_fixture srv0, srv1;struct audit_filter audit_filter;int audit_fd; };
@@ -1929,6 +1931,7 @@ FIXTURE_VARIANT_ADD(audit, ipv6) {FIXTURE_SETUP (audit) {ASSERT_EQ (0 ,set_service (&self->srv0, variant->prot,0 )); +ASSERT_EQ (0 ,set_service (&self->srv1, variant->prot,1 ));setup_loopback (_metadata);set_cap (_metadata,CAP_AUDIT_CONTROL );
@@ -1949,6 +1952,12 @@ TEST_F(audit, bind)const struct landlock_ruleset_attr ruleset_attr = { .handled_access_net =LANDLOCK_ACCESS_NET_BIND_TCP |LANDLOCK_ACCESS_NET_CONNECT_TCP , + .quiet_access_net =LANDLOCK_ACCESS_NET_CONNECT_TCP | +LANDLOCK_ACCESS_NET_BIND_TCP , + }; +const struct landlock_net_port_attr quiet_rule = { + .allowed_access =0 , + .port = self->srv1.port, };struct audit_records records;int ruleset_fd, sock_fd;
@@ -1956,6 +1965,8 @@ TEST_F(audit, bind) ruleset_fd =landlock_create_ruleset (&ruleset_attr,sizeof (ruleset_attr),0 );ASSERT_LE (0 , ruleset_fd); +ASSERT_EQ (0 ,landlock_add_rule (ruleset_fd,LANDLOCK_RULE_NET_PORT , + &quiet_rule,LANDLOCK_ADD_RULE_QUIET ));enforce_ruleset (_metadata, ruleset_fd);EXPECT_EQ (0 ,close (ruleset_fd));
@@ -1963,11 +1974,21 @@ TEST_F(audit, bind)ASSERT_LE (0 , sock_fd);EXPECT_EQ (-EACCES ,bind_variant (sock_fd, &self->srv0));EXPECT_EQ (0 ,matches_log_tcp (self->audit_fd,"net\\.bind_tcp" ,"saddr" , - variant->addr,"src" )); + variant->addr,"src" , self->srv0.port)); +/* No other logs expected. */ +EXPECT_EQ (0 ,audit_count_records (self->audit_fd, &records)); +EXPECT_EQ (0 , records.access); + +EXPECT_EQ (0 ,close (sock_fd)); + + sock_fd =socket_variant (&self->srv1); +ASSERT_LE (0 , sock_fd); +EXPECT_EQ (-EACCES ,bind_variant (sock_fd, &self->srv1)); + +/* No log expected due to quiet rule. */ EXPECT_EQ (0 ,audit_count_records (self->audit_fd, &records));EXPECT_EQ (0 , records.access); -EXPECT_EQ (1 , records.domain);EXPECT_EQ (0 ,close (sock_fd)); }
@@ -1977,6 +1998,12 @@ TEST_F(audit, connect)const struct landlock_ruleset_attr ruleset_attr = { .handled_access_net =LANDLOCK_ACCESS_NET_BIND_TCP |LANDLOCK_ACCESS_NET_CONNECT_TCP , + .quiet_access_net =LANDLOCK_ACCESS_NET_CONNECT_TCP | +LANDLOCK_ACCESS_NET_BIND_TCP , + }; +const struct landlock_net_port_attr quiet_rule = { + .allowed_access =0 , + .port = self->srv1.port, };struct audit_records records;int ruleset_fd, sock_fd;
@@ -1984,18 +2011,92 @@ TEST_F(audit, connect) ruleset_fd =landlock_create_ruleset (&ruleset_attr,sizeof (ruleset_attr),0 );ASSERT_LE (0 , ruleset_fd); +ASSERT_EQ (0 ,landlock_add_rule (ruleset_fd,LANDLOCK_RULE_NET_PORT , + &quiet_rule,LANDLOCK_ADD_RULE_QUIET ));enforce_ruleset (_metadata, ruleset_fd);EXPECT_EQ (0 ,close (ruleset_fd)); sock_fd =socket_variant (&self->srv0);ASSERT_LE (0 , sock_fd);EXPECT_EQ (-EACCES ,connect_variant (sock_fd, &self->srv0)); -EXPECT_EQ (0 ,matches_log_tcp (self->audit_fd,"net\\.connect_tcp" , -"daddr" , variant->addr,"dest" )); +EXPECT_EQ (0 , +matches_log_tcp (self->audit_fd,"net\\.connect_tcp" ,"daddr" , + variant->addr,"dest" , self->srv0.port)); + +/* No other logs expected. */ +EXPECT_EQ (0 ,audit_count_records (self->audit_fd, &records)); +EXPECT_EQ (0 , records.access); + +EXPECT_EQ (0 ,close (sock_fd)); + + sock_fd =socket_variant (&self->srv1); +ASSERT_LE (0 , sock_fd); +EXPECT_EQ (-EACCES ,connect_variant (sock_fd, &self->srv1)); + +/* Quieted - no logs expected. */ +EXPECT_EQ (0 ,audit_count_records (self->audit_fd, &records)); +EXPECT_EQ (0 , records.access); + +EXPECT_EQ (0 ,close (sock_fd)); +} + +/* Quieting bind access has no effect on connect. */ +TEST_F(audit, connect_quiet_bind) +{ +const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net =LANDLOCK_ACCESS_NET_BIND_TCP | +LANDLOCK_ACCESS_NET_CONNECT_TCP , + .quiet_access_net =LANDLOCK_ACCESS_NET_BIND_TCP , + }; +const struct landlock_ruleset_attr ruleset_attr_2 = { + .handled_access_net =LANDLOCK_ACCESS_NET_BIND_TCP | +LANDLOCK_ACCESS_NET_CONNECT_TCP , + .quiet_access_net =LANDLOCK_ACCESS_NET_CONNECT_TCP , + }; +const struct landlock_net_port_attr quiet_rule = { + .allowed_access =0 , + .port = self->srv1.port, + }; +struct audit_records records; +int ruleset_fd, sock_fd; + + ruleset_fd = +landlock_create_ruleset (&ruleset_attr,sizeof (ruleset_attr),0 ); +ASSERT_LE (0 , ruleset_fd); +ASSERT_EQ (0 ,landlock_add_rule (ruleset_fd,LANDLOCK_RULE_NET_PORT , + &quiet_rule,LANDLOCK_ADD_RULE_QUIET )); +enforce_ruleset (_metadata, ruleset_fd); +EXPECT_EQ (0 ,close (ruleset_fd)); + + sock_fd =socket_variant (&self->srv1); +ASSERT_LE (0 , sock_fd); +EXPECT_EQ (-EACCES ,connect_variant (sock_fd, &self->srv1)); +EXPECT_EQ (0 , +matches_log_tcp (self->audit_fd,"net\\.connect_tcp" ,"daddr" , + variant->addr,"dest" , self->srv1.port)); + +/* No other logs expected. */ +EXPECT_EQ (0 ,audit_count_records (self->audit_fd, &records)); +EXPECT_EQ (0 , records.access); + +EXPECT_EQ (0 ,close (sock_fd)); + +/* New layer that also denies connect but has the correct quiet bit. */ + ruleset_fd = +landlock_create_ruleset (&ruleset_attr_2,sizeof (ruleset_attr_2),0 ); +ASSERT_LE (0 , ruleset_fd); +ASSERT_EQ (0 ,landlock_add_rule (ruleset_fd,LANDLOCK_RULE_NET_PORT , + &quiet_rule,LANDLOCK_ADD_RULE_QUIET )); +enforce_ruleset (_metadata, ruleset_fd); +EXPECT_EQ (0 ,close (ruleset_fd)); + + sock_fd =socket_variant (&self->srv1); +ASSERT_LE (0 , sock_fd); +EXPECT_EQ (-EACCES ,connect_variant (sock_fd, &self->srv1)); +/* Quieted - no logs expected. */ EXPECT_EQ (0 ,audit_count_records (self->audit_fd, &records));EXPECT_EQ (0 , records.access); -EXPECT_EQ (1 , records.domain);EXPECT_EQ (0 ,close (sock_fd)); }
Signature · 1 line
2.51.1