* Re: [PATCH] Add LoadPin support for eBPF program loading
From: Alexei Starovoitov @ 2026-06-12 15:20 UTC (permalink / raw)
To: David Windsor
Cc: alex.roberts109, Kees Cook, Paul Moore, James Morris,
Serge E . Hallyn, LKML, LSM List, bpf, Alexei Starovoitov,
KP Singh
In-Reply-To: <20260612000825.105100-1-dwindsor@gmail.com>
On Thu, Jun 11, 2026 at 5:08 PM David Windsor <dwindsor@gmail.com> wrote:
>
> On Thu, Jun 11, 2026 at 01:59:10PM -0500, Alex Roberts wrote:
> > +static int loadpin_bpf_prog_load(struct bpf_prog *prog, union bpf_attr *attr,
> > + struct bpf_token *token, bool is_kernel)
> > +{
> > + int res = 0;
> > + struct file *exe_file = NULL;
> > + struct mm_struct *mm = current->mm;
> > +
> > + if (is_kernel || !mm)
> > + return 0;
> > +
> > + exe_file = get_mm_exe_file(mm);
> > + if (!exe_file)
> > + return 0;
> > +
> > + res = loadpin_check(exe_file, READING_EBPF);
>
> Why are we checking current here? IIUC this will be whoever calls
> bpf(2), which would be the loader, which would then be able to load bpf
> programs from an untrusted source.
>
> In the kmod case loadpin_check() sees the .ko itself.
See sashiko comments:
- [High] The LoadPin eBPF trust mechanism can be trivially bypassed
using standard system interpreters like the dynamic linker (`ld.so`).
- [High] The LoadPin eBPF trust mechanism can be bypassed by a
privileged attacker using prctl(PR_SET_MM_EXE_FILE).
and the bot is correct.
This patch is pointless.
^ permalink raw reply
* Re: [PATCH bpf-next 0/5] Verify BPF signed loader at load time
From: Daniel Borkmann @ 2026-06-12 9:33 UTC (permalink / raw)
To: Paul Moore
Cc: ast, kpsingh, James.Bottomley, bboscaccy, memxor, torvalds, bpf,
linux-security-module
In-Reply-To: <CAHC9VhTA7tOp16wGzb1QYnkJsVz1X3oNcmT6bb_9334=B2smEw@mail.gmail.com>
On 6/12/26 12:56 AM, Paul Moore wrote:
> On Wed, Jun 10, 2026 at 7:03 PM Daniel Borkmann <daniel@iogearbox.net> wrote:
>>
>> The BPF signing scheme signs a light skeleton's loader program and lets
>> the loader vouch for everything else: bpftool bakes the SHA256 of the
>> metadata map into the loader's instructions, signs the instructions, and
>> the loader compares the (frozen, exclusive) map against that hash from
>> within BPF once it runs. The construction is sound as a trusted hash
>> chain, but the kernel itself never attests the metadata, and that split
>> has been the recurring objection from the LSM / integrity side since the
>> scheme was proposed.
>>
>> This proposal closes both gaps by having the kernel verify the metadata
>> at BPF_PROG_LOAD time, before the LSM admission hook and before the
>> verifier, /without/ growing the UAPI. A signed loader binds its metadata
>> map(s) through the existing fd_array/fd_array_cnt, and exclusive maps
>> are already bound to the loader's digest via excl_prog_hash. When a
>> signature is present, the kernel collects the exclusive maps from the
>> fd_array and appends their frozen contents to the instructions before
>> PKCS#7 verification, so the signature covers ...
>>
>> insns || metadata_0 || metadata_1 || [...]
>>
>> ... in fd_array order. The in-loader hash check is dropped from the
>> gen_loader entirely: generated loaders carry no verification logic
>> anymore, and signing or verifying a skeleton becomes an ordinary CMS
>> operation over bytes that sit verbatim in the skeleton, reproducible
>> offline. A signed program is either BPF_SIG_UNSIGNED or BPF_SIG_VERIFIED
>> with nothing in between.
>
> I'll be honest and say I'm a bit surprised to see this patchset,
> especially since KP and Alexei argued so strongly against this
> signature scheme, preferring KP's scheme where the loader verified the
> maps. I'd be curious to hear the reason for the change of heart if
> you can share it. Regardless of the motivation for this change, I
> obviously think this is a significant improvement over KP's signature
> scheme which shipped in Linux v6.18.
>
> I also think it is worth mentioning the similarities to work Blaise
> did before the most recent Hornet version:
>
> https://lore.kernel.org/linux-security-module/20250929213520.1821223-1-bboscaccy@linux.microsoft.com/
>
> While Blaise's patchset added to the UAPI, that was done simply to
> retain compatibility with KP's signature scheme; your patchset does
> without any UAPI additions, but loses compatibility with existing
> signed lskels. Beyond that, the basic signature scheme between
> Blaise's patchset and what you are proposing appears the same ...
> which is a good thing as far as I'm concerned.
For the rework, I've mainly been looking at KP's most recent series as
well as some of the past discussions. Mainly trying to see if we can get
away with a simpler model and without having to pull in BTF support for
the signed loader just for the extra kfunc. Over the last couple of weeks,
I've also build user space tooling on my side to experiment with some
real world application which the signed loader would have to deal with.
For sending a v2, I'll have to move this slightly deeper into verifier
side but before the main verifier work happens in order to solve the
exclusive map race issue, and additionally we could also utilize the
verifier log to tell the user that the signature validation failed.
I think a couple of things would still stand out imo: i) we sign over the
raw bytes, not the derived hash anymore, so the hashing is only used in the
context to tie the map to the loader prog, but not anymore for the signature.
ii) its one single scheme and not a parallel branch, so the main loader is
built upon the updated signing scheme rather than having this as an option
on the side; iow this replaces the in-loader check and there's a single PKCS#7-
over-bytes path, not an 'if (signature_maps_size)' fork; and iii) given we
expose the verification result in the BPF prog, we also don't need a new LSM
hook and can just piggy back on the existing security_bpf_prog() which also
has the possibility to still reject late at this point.
Anyway, I hope we can find a compromise here.. will keep you posted on v2
of the series.
Thanks,
Daniel
^ permalink raw reply
* Re: [PATCH v3 1/3] landlock: Require LANDLOCK_ACCESS_FS_MAKE_WHITEOUT for RENAME_WHITEOUT
From: Günther Noack @ 2026-06-12 8:34 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Christian Brauner, linux-security-module, Paul Moore,
Amir Goldstein, Miklos Szeredi, Serge Hallyn, Stephen Smalley
In-Reply-To: <20260610.uoMee2quoo9k@digikod.net>
Thanks for the review!
On Wed, Jun 10, 2026 at 03:38:56PM +0200, Mickaël Salaün wrote:
> Making MAKE_CHAR not covering MAKE_WHITEOUT is not addressed (see
> previous discussion). MAKE_CHAR should not restrict whiteout creation
> *if* MAKE_WHITEOUT is handled.
(This is option (3) from your reply to V1 [1].)
I am skeptical of this approach, because it complicates how userspace
needs to deal with this access right. Consider the following
scenario: A program wants to install the policy:
* DENY MAKE_WHITEOUT, MAKE_CHAR
* ALLOW MAKE_WHITEOUT in /foo (path_beneath rule)
Then, if the kernel ABI predates make-whiteout, with the usual
best-effort fallback (clearing out the unsupported bits), this ruleset
becomes:
* DENY MAKE_CHAR
* (no ALLOW rule)
But this ruleset is incorrect, because it denies mknod("/foo/x",
S_IFCHR | mode, makedev(0, 0)) in /foo, which was explicitly allowed
in the earlier ruleset.
So in order to implement the best-effort fallback, I guess userspace
libraries would now have to take into account whether there are any
rules where MAKE_WHITEOUT is specifically allowed, and if so, they
can't restrict MAKE_CHAR either? I find this a bit complicated and I
think it's foreseeable that library implementers will predominantly
get this wrong.
Let me circle back to the other options you mentioned in [1], quoting
them here for reference:
> I see four options:
>
> 1. Consider whiteouts as regular files and make them handled by
> LANDLOCK_ACCESS_FS_MAKE_REG. This would require an erratum and would
> make sense for direct mknod calls, but it would be weird for
> renameat2 calls than move a file and should only require
> LANDLOCK_ACCESS_FS_REMOVE_FILE from the user point of view.
It would be weird for renameat2 calls to require MAKE_REG in the
source directory, but the weirdness would only affect
fuse-overlayfs-style programs and could be documented explicitly for
them for the case that they start using Landlock.
Normal programs that just call rename() on an existing FUSE-Overlayfs
filesystem would *not* require the MAKE_REG right, because the FUSE
process would do that on their behalf with the FUSE processes'
credentials.
>
> 2. Add a new LANDLOCK_ACCESS_FS_MAKE_WHITEOUT right to handle whitout
> creation (direct and indirect?) and keep LANDLOCK_ACCESS_FS_MAKE_CHAR
> handle direct whiteout creation (and don't backport anything). It
> looks inconsistent from an access control point of view.
MAKE_WHITEOUT to handle rename(RENAME_WHITEOUT) and MAKE_CHAR to
handle mknod(chardev (0, 0)) -- This is a bit inconsistent, but it
does not make a difference for any programs other than the ones
calling rename(RENAME_WHITEOUT) (i.e., overlayfs-fuse), and it could
be documented for that one use case.
I find this a pragmatic balance, and it does not require special logic
for the best-effort fallback either. Could you be persuaded to go
this route instead?
> 3. Add a new LANDLOCK_ACCESS_FS_MAKE_WHITEOUT right and, when handled,
> make LANDLOCK_ACCESS_FS_MAKE_CHAR not handle whiteout. This would be
> a bit weird from a kernel point of view but it should work well for
> users while still forbidding direct whiteout creation.
Except for the best-effort fallback, which is IMHO prone to
implementation bugs. (see above)
On the side, the implementation of this is also non-trivial: In order
to check for mknod(..., makedev(0, 0)), we need to check
layer-by-layer whether the layer handles MAKE_WHITEOUT and then either
check for MAKE_CHAR or MAKE_WHITEOUT.
> 4. Add a new LANDLOCK_ACCESS_FS_MAKE_WHITEOUT right and make
> LANDLOCK_ACCESS_FS_MAKE_CHAR never handle whiteout (and backport
> MAKE_CHAR fix with an errata). This would be consistent but backport
> a way to directly create whiteouts (e.g. with mknod).
It's mostly theoretical, but lifting the mknod(chardev (0,0))
restriction for normal mknod() calls and calling it an erratum seems
surprising as well, because it would relax security guarantees for
existing programs.
I also pondered the alternative of creating an erratum but
intentionally *not* backporting it, but even in that case, that
surprising erratum still affects older programs which are deployed on
newer kernels.
Revisiting this discussion, I'd lean towards option 1 or 2 -- could
you be persuaded towards one of these?
I have a slight preference for option 1 (using MAKE_REG) because it
would be a narrow fix that could be backported to older kernels as
well and would not require a new access right. Given that the use
case for RENAME_WHITEOUT is really only for FUSE-OverlayFS and given
that FUSE-OverlayFS anyway needs MAKE_REG permissions there, I have
trouble imagining a scenario where a separate access right for
MAKE_WHITEOUT is needed in a policy. It seems like a pragmatic
choice.
> Specific tests should check that all
> these cases are proprely handled.
>
> There is no documentation update related to the new feature. A note
> should also explain what exactly is a whiteout and why it is not
> considered a character device (see previous discussions).
>
> The sandboxer is not updated.
>
> There is no audit tests.
Acknowledged, these were missing.
(I was initially hoping that this bug report wouldn't expand into a
full-fledged feature with its own access right constant, but it is
correct that this is all required in that case... :-/)
Will add this for the next patch set revision if it is still needed.
—Günther
[1] https://lore.kernel.org/all/20260414.Lae5ida1eeGh@digikod.net/
^ permalink raw reply
* Re: [PATCH] Add LoadPin support for eBPF program loading
From: kernel test robot @ 2026-06-12 6:17 UTC (permalink / raw)
To: Alex Roberts via B4 Relay, Kees Cook, Paul Moore, James Morris,
Serge E. Hallyn
Cc: oe-kbuild-all, linux-kernel, linux-security-module, bpf,
Alexei Starovoitov, KP Singh, Alex Roberts
In-Reply-To: <20260611-b4-rfc-loadpin-ebpf-v1-1-11a6c8e6170d@outlook.com>
Hi Alex,
kernel test robot noticed the following build errors:
[auto build test ERROR on 122b52f0bab007ebeb414c8280c1def17b9ed1f4]
url: https://github.com/intel-lab-lkp/linux/commits/Alex-Roberts-via-B4-Relay/Add-LoadPin-support-for-eBPF-program-loading/20260612-031559
base: 122b52f0bab007ebeb414c8280c1def17b9ed1f4
patch link: https://lore.kernel.org/r/20260611-b4-rfc-loadpin-ebpf-v1-1-11a6c8e6170d%40outlook.com
patch subject: [PATCH] Add LoadPin support for eBPF program loading
config: nios2-randconfig-002-20260612 (https://download.01.org/0day-ci/archive/20260612/202606121426.NrJtdO3K-lkp@intel.com/config)
compiler: nios2-linux-gcc (GCC) 11.5.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20260612/202606121426.NrJtdO3K-lkp@intel.com/reproduce)
If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202606121426.NrJtdO3K-lkp@intel.com/
All errors (new ones prefixed by >>):
In file included from <command-line>:
security/selinux/hooks.c: In function 'selinux_kernel_read_file':
>> include/linux/compiler_types.h:699:45: error: call to '__compiletime_assert_916' declared with attribute error: New kernel_read_file_id introduced; update SELinux!
699 | _compiletime_assert(condition, msg, __compiletime_assert_, __COUNTER__)
| ^
include/linux/compiler_types.h:680:25: note: in definition of macro '__compiletime_assert'
680 | prefix ## suffix(); \
| ^~~~~~
include/linux/compiler_types.h:699:9: note: in expansion of macro '_compiletime_assert'
699 | _compiletime_assert(condition, msg, __compiletime_assert_, __COUNTER__)
| ^~~~~~~~~~~~~~~~~~~
include/linux/build_bug.h:40:37: note: in expansion of macro 'compiletime_assert'
40 | #define BUILD_BUG_ON_MSG(cond, msg) compiletime_assert(!(cond), msg)
| ^~~~~~~~~~~~~~~~~~
security/selinux/hooks.c:4412:9: note: in expansion of macro 'BUILD_BUG_ON_MSG'
4412 | BUILD_BUG_ON_MSG(READING_MAX_ID > 8,
| ^~~~~~~~~~~~~~~~
security/selinux/hooks.c: In function 'selinux_kernel_load_data':
>> include/linux/compiler_types.h:699:45: error: call to '__compiletime_assert_917' declared with attribute error: New kernel_load_data_id introduced; update SELinux!
699 | _compiletime_assert(condition, msg, __compiletime_assert_, __COUNTER__)
| ^
include/linux/compiler_types.h:680:25: note: in definition of macro '__compiletime_assert'
680 | prefix ## suffix(); \
| ^~~~~~
include/linux/compiler_types.h:699:9: note: in expansion of macro '_compiletime_assert'
699 | _compiletime_assert(condition, msg, __compiletime_assert_, __COUNTER__)
| ^~~~~~~~~~~~~~~~~~~
include/linux/build_bug.h:40:37: note: in expansion of macro 'compiletime_assert'
40 | #define BUILD_BUG_ON_MSG(cond, msg) compiletime_assert(!(cond), msg)
| ^~~~~~~~~~~~~~~~~~
security/selinux/hooks.c:4449:9: note: in expansion of macro 'BUILD_BUG_ON_MSG'
4449 | BUILD_BUG_ON_MSG(LOADING_MAX_ID > 8,
| ^~~~~~~~~~~~~~~~
vim +/__compiletime_assert_916 +699 include/linux/compiler_types.h
eb5c2d4b45e3d2 Will Deacon 2020-07-21 685
eb5c2d4b45e3d2 Will Deacon 2020-07-21 686 #define _compiletime_assert(condition, msg, prefix, suffix) \
eb5c2d4b45e3d2 Will Deacon 2020-07-21 687 __compiletime_assert(condition, msg, prefix, suffix)
eb5c2d4b45e3d2 Will Deacon 2020-07-21 688
eb5c2d4b45e3d2 Will Deacon 2020-07-21 689 /**
eb5c2d4b45e3d2 Will Deacon 2020-07-21 690 * compiletime_assert - break build and emit msg if condition is false
eb5c2d4b45e3d2 Will Deacon 2020-07-21 691 * @condition: a compile-time constant condition to check
eb5c2d4b45e3d2 Will Deacon 2020-07-21 692 * @msg: a message to emit if condition is false
eb5c2d4b45e3d2 Will Deacon 2020-07-21 693 *
eb5c2d4b45e3d2 Will Deacon 2020-07-21 694 * In tradition of POSIX assert, this macro will break the build if the
eb5c2d4b45e3d2 Will Deacon 2020-07-21 695 * supplied condition is *false*, emitting the supplied error message if the
eb5c2d4b45e3d2 Will Deacon 2020-07-21 696 * compiler has support to do so.
eb5c2d4b45e3d2 Will Deacon 2020-07-21 697 */
eb5c2d4b45e3d2 Will Deacon 2020-07-21 698 #define compiletime_assert(condition, msg) \
eb5c2d4b45e3d2 Will Deacon 2020-07-21 @699 _compiletime_assert(condition, msg, __compiletime_assert_, __COUNTER__)
eb5c2d4b45e3d2 Will Deacon 2020-07-21 700
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
^ permalink raw reply
* [PATCH] apparmor: fix use-after-free in policy replacement path
From: Junxiao Chang @ 2026-06-13 6:04 UTC (permalink / raw)
To: john.johansen, paul, jmorris, serge, apparmor,
linux-security-module, linux-kernel
Cc: junxiao.chang
A use-after-free issue can be triggered when running the
following stress-ng workload:
```
sudo stress-ng --apparmor 0 --timeout 30 \
--oom-avoid-bytes 10% --skip-silent --verbose
```
The warning looks like:
```
refcount_t: addition on 0; use-after-free
aa_replace_profiles+0xbe5/0x12a0
policy_update+0xdb/0x170
profile_replace+0x4b/0xb0
```
The issue can be reproduced on both v7.1-rc7 and Ubuntu
6.17.0-35-generic kernels.
aa_get_profile_loaddata() requires the supplied loaddata object
to hold a valid reference. However, the loaddata reference count
may already have reached zero in the replacement loop, resulting
in a use-after-free condition.
Avoid calling aa_get_profile_loaddata() on loaddata objects with
a zero reference count and skip those entries instead.
Fixes: a0b7091c4de4 ("apparmor: fix race on rawdata dereference")
Signed-off-by: Junxiao Chang <junxiao.chang@intel.com>
---
security/apparmor/policy.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/security/apparmor/policy.c b/security/apparmor/policy.c
index b6a5eb4021dbd..98f84d4552697 100644
--- a/security/apparmor/policy.c
+++ b/security/apparmor/policy.c
@@ -1220,7 +1220,7 @@ ssize_t aa_replace_profiles(struct aa_ns *policy_ns, struct aa_label *label,
/* check for duplicate rawdata blobs: space and file dedup */
if (!list_empty(&ns->rawdata_list)) {
list_for_each_entry(rawdata_ent, &ns->rawdata_list, list) {
- if (aa_rawdata_eq(rawdata_ent, udata)) {
+ if (kref_read(&rawdata_ent->pcount) && aa_rawdata_eq(rawdata_ent, udata)) {
struct aa_loaddata *tmp;
tmp = aa_get_profile_loaddata(rawdata_ent);
--
2.43.0
^ permalink raw reply related
* [PATCH v11 9/9] selftests/landlock: Add tests for invalid use of quiet flag
From: Tingmao Wang @ 2026-06-12 1:48 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1781228815.git.m@maowtm.org>
Make sure that these calls return EINVAL.
Assisted-by: GitHub-Copilot:claude-opus-4.8
Signed-off-by: Tingmao Wang <m@maowtm.org>
---
Changes in v11:
- Test net/scope quiet fields too
Changes in v4:
- New patch
tools/testing/selftests/landlock/base_test.c | 116 +++++++++++++++++++
1 file changed, 116 insertions(+)
diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
index 84e91fcaa1b2..cbd3c1669951 100644
--- a/tools/testing/selftests/landlock/base_test.c
+++ b/tools/testing/selftests/landlock/base_test.c
@@ -526,4 +526,120 @@ TEST(cred_transfer)
EXPECT_EQ(EACCES, errno);
}
+TEST(useless_quiet_rule_fs)
+{
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ .quiet_access_fs = 0,
+ };
+ struct landlock_path_beneath_attr path_beneath_attr = {
+ .allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int ruleset_fd, root_fd;
+
+ drop_caps(_metadata);
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ root_fd = open("/", O_PATH | O_CLOEXEC);
+ ASSERT_LE(0, root_fd);
+ path_beneath_attr.parent_fd = root_fd;
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath_attr,
+ LANDLOCK_ADD_RULE_QUIET));
+ ASSERT_EQ(EINVAL, errno);
+
+ /* Check that the rule had not been added. */
+ ASSERT_EQ(0, close(root_fd));
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ ASSERT_EQ(-1, open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC));
+ ASSERT_EQ(EACCES, errno);
+}
+
+TEST(useless_quiet_rule_net)
+{
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+ .quiet_access_net = 0,
+ };
+ struct landlock_net_port_attr net_port_attr = {
+ .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
+ .port = 1024,
+ };
+ int ruleset_fd;
+
+ drop_caps(_metadata);
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ ASSERT_EQ(-1,
+ landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+ &net_port_attr, LANDLOCK_ADD_RULE_QUIET));
+ ASSERT_EQ(EINVAL, errno);
+
+ ASSERT_EQ(0, close(ruleset_fd));
+}
+
+TEST(invalid_quiet_bits_1)
+{
+ const struct landlock_ruleset_attr ruleset_attr_fs = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ .quiet_access_fs = LANDLOCK_ACCESS_FS_WRITE_FILE,
+ };
+ const struct landlock_ruleset_attr ruleset_attr_net = {
+ .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+ .quiet_access_net = LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ };
+ const struct landlock_ruleset_attr ruleset_attr_scoped = {
+ .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
+ .quiet_scoped = LANDLOCK_SCOPE_SIGNAL,
+ };
+
+ /* Quiet bit set but not part of the handled mask. */
+ ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_fs,
+ sizeof(ruleset_attr_fs), 0));
+ ASSERT_EQ(EINVAL, errno);
+
+ ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_net,
+ sizeof(ruleset_attr_net), 0));
+ ASSERT_EQ(EINVAL, errno);
+
+ ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_scoped,
+ sizeof(ruleset_attr_scoped), 0));
+ ASSERT_EQ(EINVAL, errno);
+}
+
+TEST(invalid_quiet_bits_2)
+{
+ const struct landlock_ruleset_attr ruleset_attr_fs = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ .quiet_access_fs = 1ULL << 63,
+ };
+ const struct landlock_ruleset_attr ruleset_attr_net = {
+ .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+ .quiet_access_net = 1ULL << 63,
+ };
+ const struct landlock_ruleset_attr ruleset_attr_scoped = {
+ .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
+ .quiet_scoped = 1ULL << 63,
+ };
+
+ /* Quiet bit outside of the valid access range. */
+ ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_fs,
+ sizeof(ruleset_attr_fs), 0));
+ ASSERT_EQ(EINVAL, errno);
+
+ ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_net,
+ sizeof(ruleset_attr_net), 0));
+ ASSERT_EQ(EINVAL, errno);
+
+ ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_scoped,
+ sizeof(ruleset_attr_scoped), 0));
+ ASSERT_EQ(EINVAL, errno);
+}
+
TEST_HARNESS_MAIN
--
2.54.0
^ permalink raw reply related
* [PATCH v11 8/9] selftests/landlock: Add tests for quiet flag with scope
From: Tingmao Wang @ 2026-06-12 1:48 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1781228815.git.m@maowtm.org>
Enhance scoped_audit.connect_to_child and audit_flags.signal to test
interaction with various quiet flag settings.
Signed-off-by: Tingmao Wang <m@maowtm.org>
---
Changes in v4:
- New patch
tools/testing/selftests/landlock/audit_test.c | 25 ++++--
.../landlock/scoped_abstract_unix_test.c | 77 ++++++++++++++++---
2 files changed, 87 insertions(+), 15 deletions(-)
diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
index d55fe4c5b531..3b344b03199e 100644
--- a/tools/testing/selftests/landlock/audit_test.c
+++ b/tools/testing/selftests/landlock/audit_test.c
@@ -794,30 +794,42 @@ FIXTURE(audit_flags)
FIXTURE_VARIANT(audit_flags)
{
const int restrict_flags;
+ const __u64 quiet_scoped;
};
/* clang-format off */
FIXTURE_VARIANT_ADD(audit_flags, default) {
/* clang-format on */
.restrict_flags = 0,
+ .quiet_scoped = 0,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(audit_flags, same_exec_off) {
/* clang-format on */
.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF,
+ .quiet_scoped = 0,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(audit_flags, subdomains_off) {
/* clang-format on */
.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF,
+ .quiet_scoped = 0,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(audit_flags, cross_exec_on) {
/* clang-format on */
.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON,
+ .quiet_scoped = 0,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(audit_flags, signal_quieted) {
+ /* clang-format on */
+ .restrict_flags = 0,
+ .quiet_scoped = LANDLOCK_SCOPE_SIGNAL,
};
FIXTURE_SETUP(audit_flags)
@@ -861,12 +873,16 @@ TEST_F(audit_flags, signal)
pid_t child;
struct audit_records records;
__u64 deallocated_dom = 2;
+ bool expect_audit = !(variant->restrict_flags &
+ LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) &&
+ !(variant->quiet_scoped & LANDLOCK_SCOPE_SIGNAL);
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
const struct landlock_ruleset_attr ruleset_attr = {
.scoped = LANDLOCK_SCOPE_SIGNAL,
+ .quiet_scoped = variant->quiet_scoped,
};
int ruleset_fd;
@@ -883,8 +899,7 @@ TEST_F(audit_flags, signal)
EXPECT_EQ(-1, kill(getppid(), 0));
EXPECT_EQ(EPERM, errno);
- if (variant->restrict_flags &
- LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) {
+ if (!expect_audit) {
EXPECT_EQ(-EAGAIN, matches_log_signal(
_metadata, self->audit_fd,
getppid(), self->domain_id));
@@ -911,8 +926,7 @@ TEST_F(audit_flags, signal)
/* Makes sure there is no superfluous logged records. */
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
- if (variant->restrict_flags &
- LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) {
+ if (!expect_audit) {
EXPECT_EQ(0, records.access);
} else {
EXPECT_EQ(1, records.access);
@@ -936,8 +950,7 @@ TEST_F(audit_flags, signal)
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
- if (variant->restrict_flags &
- LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) {
+ if (!expect_audit) {
/*
* No deallocation record: denials=0 never matches a real
* record.
diff --git a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c
index 72f97648d4a7..d16555f7b0d3 100644
--- a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c
+++ b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c
@@ -293,6 +293,45 @@ FIXTURE_TEARDOWN_PARENT(scoped_audit)
EXPECT_EQ(0, audit_cleanup(-1, NULL));
}
+FIXTURE_VARIANT(scoped_audit)
+{
+ const __u64 scoped;
+ const __u64 quiet_scoped;
+};
+
+// clang-format off
+FIXTURE_VARIANT_ADD(scoped_audit, no_quiet)
+{
+ // clang-format on
+ .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
+ .quiet_scoped = 0,
+};
+
+// clang-format off
+FIXTURE_VARIANT_ADD(scoped_audit, quiet_abstract_socket)
+{
+ // clang-format on
+ .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
+ .quiet_scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
+};
+
+// clang-format off
+FIXTURE_VARIANT_ADD(scoped_audit, quiet_abstract_socket_2)
+{
+ // clang-format on
+ .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL,
+ .quiet_scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
+ LANDLOCK_SCOPE_SIGNAL,
+};
+
+// clang-format off
+FIXTURE_VARIANT_ADD(scoped_audit, quiet_unrelated)
+{
+ // clang-format on
+ .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL,
+ .quiet_scoped = LANDLOCK_SCOPE_SIGNAL,
+};
+
/* python -c 'print(b"\0selftests-landlock-abstract-unix-".hex().upper())' */
#define ABSTRACT_SOCKET_PATH_PREFIX \
"0073656C6674657374732D6C616E646C6F636B2D61627374726163742D756E69782D"
@@ -308,6 +347,13 @@ TEST_F(scoped_audit, connect_to_child)
char buf;
int dgram_client;
struct audit_records records;
+ int ruleset_fd;
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .scoped = variant->scoped,
+ .quiet_scoped = variant->quiet_scoped,
+ };
+ bool should_audit =
+ !(variant->quiet_scoped & LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
/* Makes sure there is no superfluous logged records. */
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
@@ -345,7 +391,14 @@ TEST_F(scoped_audit, connect_to_child)
EXPECT_EQ(0, close(pipe_child[1]));
EXPECT_EQ(0, close(pipe_parent[0]));
- create_scoped_domain(_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd)
+ {
+ TH_LOG("Failed to create a ruleset: %s", strerror(errno));
+ }
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
/* Signals that the parent is in a domain, if any. */
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
@@ -360,14 +413,20 @@ TEST_F(scoped_audit, connect_to_child)
EXPECT_EQ(-1, err_dgram);
EXPECT_EQ(EPERM, errno);
- EXPECT_EQ(
- 0,
- audit_match_record(
- self->audit_fd, AUDIT_LANDLOCK_ACCESS,
- REGEX_LANDLOCK_PREFIX
- " blockers=scope\\.abstract_unix_socket path=" ABSTRACT_SOCKET_PATH_PREFIX
- "[0-9A-F]\\+$",
- NULL));
+ if (should_audit) {
+ EXPECT_EQ(
+ 0,
+ audit_match_record(
+ self->audit_fd, AUDIT_LANDLOCK_ACCESS,
+ REGEX_LANDLOCK_PREFIX
+ " blockers=scope\\.abstract_unix_socket path=" ABSTRACT_SOCKET_PATH_PREFIX
+ "[0-9A-F]\\+$",
+ NULL));
+ }
+
+ /* No other logs */
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
EXPECT_EQ(0, close(dgram_client));
--
2.54.0
^ permalink raw reply related
* [PATCH v11 7/9] selftests/landlock: add tests for quiet flag with net rules
From: Tingmao Wang @ 2026-06-12 1:48 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1781228815.git.m@maowtm.org>
Tests 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.
Signed-off-by: Tingmao Wang <m@maowtm.org>
Assisted-by: GitHub-Copilot:claude-opus-4.7 copilot-review
---
Changes in v10:
- Fix comment: dst -> dest
Changes in v9:
- Rebased on top of UDP support series
Changes in v3:
- New patch
tools/testing/selftests/landlock/net_test.c | 138 ++++++++++++++++++--
1 file changed, 128 insertions(+), 10 deletions(-)
diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c
index 05b41e4da28f..d35610401954 100644
--- a/tools/testing/selftests/landlock/net_test.c
+++ b/tools/testing/selftests/landlock/net_test.c
@@ -2742,12 +2742,22 @@ TEST_F(port_specific, bind_connect_1023)
EXPECT_EQ(0, close(bind_fd));
}
+/**
+ * matches_auditlog - Check audit log for a network access denial
+ *
+ * @audit_fd: Audit file descriptor.
+ * @blockers: A regex-escaped blocker string, e.g., "net\.bind_tcp".
+ * @dir_addr: Either "saddr" or "daddr", ignored if addr is NULL.
+ * @addr: A regex-escaped IP address string, or NULL.
+ * @dir_port: Either "src" or "dest", ignored if addr is NULL.
+ * @port: A port number, ignored if addr is NULL.
+ */
static int matches_auditlog(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_with_addrport_tmpl[] = REGEX_LANDLOCK_PREFIX
- " blockers=%s %s=%s %s=1024$";
+ " blockers=%s %s=%s %s=%u$";
static const char log_without_addrport_tmpl[] = REGEX_LANDLOCK_PREFIX
" blockers=%s";
/*
@@ -2755,8 +2765,9 @@ static int matches_auditlog(const int audit_fd, const char *const blockers,
* Max strlen(dir_addr): 5
* Max strlen(addr): 12
* Max strlen(dir_port): 4
+ * Max strlen(%u port): 5
*/
- char log_match[sizeof(log_with_addrport_tmpl) + 37];
+ char log_match[sizeof(log_with_addrport_tmpl) + 42];
int log_match_len;
if (addr == NULL)
@@ -2765,7 +2776,7 @@ static int matches_auditlog(const int audit_fd, const char *const blockers,
else
log_match_len = snprintf(log_match, sizeof(log_match),
log_with_addrport_tmpl, blockers,
- dir_addr, addr, dir_port);
+ dir_addr, addr, dir_port, port);
if (log_match_len > sizeof(log_match))
return -E2BIG;
@@ -2777,6 +2788,8 @@ FIXTURE(audit)
{
struct service_fixture srv0;
struct service_fixture srv1;
+ /* srv2 has a rule with no access but quiet bit set. */
+ struct service_fixture srv2;
struct service_fixture unspec_srv0;
struct audit_filter audit_filter;
int audit_fd;
@@ -2836,6 +2849,7 @@ FIXTURE_SETUP(audit)
ASSERT_EQ(0, set_service(&self->srv0, variant->prot, 0));
ASSERT_EQ(0, set_service(&self->srv1, variant->prot, 1));
+ ASSERT_EQ(0, set_service(&self->srv2, variant->prot, 2));
ASSERT_EQ(0, set_service(&self->unspec_srv0, prot_unspec, 0));
setup_loopback(_metadata);
@@ -2866,6 +2880,11 @@ TEST_F(audit, bind)
LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_net = access_rights,
+ .quiet_access_net = access_rights,
+ };
+ const struct landlock_net_port_attr quiet_rule = {
+ .allowed_access = 0,
+ .port = self->srv2.port,
};
struct audit_records records;
int ruleset_fd, sock_fd;
@@ -2873,6 +2892,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));
@@ -2880,13 +2901,24 @@ TEST_F(audit, bind)
ASSERT_LE(0, sock_fd);
EXPECT_EQ(-EACCES, bind_variant(sock_fd, &self->srv0));
EXPECT_EQ(0, matches_auditlog(self->audit_fd, audit_evt, "saddr",
- variant->addr, "src"));
+ variant->addr, "src", self->srv0.port));
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));
+
+ /* Bind to srv2 (with quiet rule): no new audit logs. */
+ sock_fd = socket_variant(&self->srv2);
+ ASSERT_LE(0, sock_fd);
+ EXPECT_EQ(-EACCES, bind_variant(sock_fd, &self->srv2));
+
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(0, records.domain);
+
+ EXPECT_EQ(0, close(sock_fd));
}
TEST_F(audit, connect)
@@ -2903,11 +2935,16 @@ TEST_F(audit, connect)
const __u64 access_rights = bind_right | conn_right;
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_net = access_rights,
+ .quiet_access_net = access_rights,
};
const struct landlock_net_port_attr rule_connect_p1 = {
.allowed_access = conn_right,
.port = self->srv1.port,
};
+ const struct landlock_net_port_attr quiet_rule = {
+ .allowed_access = 0,
+ .port = self->srv2.port,
+ };
struct audit_records records;
int ruleset_fd, sock_fd;
@@ -2916,6 +2953,8 @@ TEST_F(audit, connect)
ASSERT_LE(0, ruleset_fd);
ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
&rule_connect_p1, 0));
+ 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));
@@ -2923,7 +2962,7 @@ TEST_F(audit, connect)
ASSERT_LE(0, sock_fd);
EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv0));
EXPECT_EQ(0, matches_auditlog(self->audit_fd, audit_evt, "daddr",
- variant->addr, "dest"));
+ variant->addr, "dest", self->srv0.port));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
@@ -2934,13 +2973,91 @@ TEST_F(audit, connect)
EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv1));
EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.bind_udp",
- NULL, NULL, NULL));
+ NULL, NULL, NULL, 0));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(0, records.domain);
}
EXPECT_EQ(0, close(sock_fd));
+
+ /* Connect to srv2 (with quiet rule): no new audit logs. */
+ sock_fd = socket_variant(&self->srv2);
+ ASSERT_LE(0, sock_fd);
+ EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv2));
+
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(0, records.domain);
+
+ EXPECT_EQ(0, close(sock_fd));
+}
+
+/* Quieting bind access has no effect on connect. */
+TEST_F(audit, connect_quiet_bind)
+{
+ const char *audit_evt = (variant->prot.type == SOCK_STREAM ?
+ "net\\.connect_tcp" :
+ "net\\.connect_send_udp");
+ const int bind_right = (variant->prot.type == SOCK_STREAM ?
+ LANDLOCK_ACCESS_NET_BIND_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP);
+ const int conn_right = (variant->prot.type == SOCK_STREAM ?
+ LANDLOCK_ACCESS_NET_CONNECT_TCP :
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
+ const int access_rights = bind_right | conn_right;
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net = access_rights,
+ .quiet_access_net = bind_right,
+ };
+ const struct landlock_ruleset_attr ruleset_attr_2 = {
+ .handled_access_net = access_rights,
+ .quiet_access_net = conn_right,
+ };
+ const struct landlock_net_port_attr quiet_rule = {
+ .allowed_access = 0,
+ .port = self->srv2.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->srv2);
+ ASSERT_LE(0, sock_fd);
+ EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv2));
+ EXPECT_EQ(0, matches_auditlog(self->audit_fd, audit_evt, "daddr",
+ variant->addr, "dest", self->srv2.port));
+
+ 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->srv2);
+ ASSERT_LE(0, sock_fd);
+ EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv2));
+
+ /* 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));
}
TEST_F(audit, sendmsg)
@@ -2973,7 +3090,8 @@ TEST_F(audit, sendmsg)
ASSERT_LE(0, sock_fd);
EXPECT_EQ(-EACCES, sendto_variant(sock_fd, &self->srv0, "A", 1, 0));
EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.connect_send_udp",
- "daddr", variant->addr, "dest"));
+ "daddr", variant->addr, "dest",
+ self->srv0.port));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
@@ -2982,7 +3100,7 @@ TEST_F(audit, sendmsg)
/* Check that autobind generates a denied bind event. */
EXPECT_EQ(-EACCES, sendto_variant(sock_fd, &self->srv1, "A", 1, 0));
EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.bind_udp", NULL,
- NULL, NULL));
+ NULL, NULL, 0));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(0, records.domain);
@@ -2990,7 +3108,7 @@ TEST_F(audit, sendmsg)
EXPECT_EQ(-EACCES,
sendto_variant(sock_fd, &self->unspec_srv0, "B", 1, 0));
EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.connect_send_udp",
- "daddr", NULL, "dest"));
+ "daddr", NULL, "dest", 0));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(0, records.domain);
--
2.54.0
^ permalink raw reply related
* [PATCH v11 6/9] selftests/landlock: add tests for quiet flag with fs rules
From: Tingmao Wang @ 2026-06-12 1:48 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1781228815.git.m@maowtm.org>
Test 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.
Assisted-by: GitHub-Copilot:claude-opus-4.6 copilot-review
Signed-off-by: Tingmao Wang <m@maowtm.org>
---
Changes in v11:
- Remove broken if brackets change
Changes in v10:
- Fix grammar in some comments
- if brackets
Changes in v8:
- Rebase, resolve conflict, then clang-format
- Remove previously added comment about domain allocation record leakage -
this is now documented properly by 239fd9a6f948 ("selftests/landlock:
Drain stale audit records on init")
- Fix missing EXPECT_EQ on audit_count_records() return value
Changes in v6:
- Change quiet bool argument of add_path_beneath into a __u32 flags
(suggested by Justin Suess)
- Rename quiet_behind_mountpoint_ignored_disconnected to
quiet_behind_mountpoint_disconnected and fix test due to disconnected
directory handling changes
Changes in v5:
- Add quiet_two_layers_different_handled_{1,2,3} variants.
Changes in v3:
- New patch
tools/testing/selftests/landlock/fs_test.c | 2448 +++++++++++++++++++-
1 file changed, 2439 insertions(+), 9 deletions(-)
diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index 10d9355ade5f..702f0dba462e 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -720,7 +720,7 @@ 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, __u32 flags)
{
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = allowed_access,
@@ -733,7 +733,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));
@@ -780,7 +780,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, 0);
}
return ruleset_fd;
}
@@ -1310,7 +1310,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, 0);
/*
* According to ruleset_fd, dir_s1d2 should now have the
* LANDLOCK_ACCESS_FS_READ_FILE and LANDLOCK_ACCESS_FS_WRITE_FILE
@@ -1342,7 +1342,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, 0);
enforce_ruleset(_metadata, ruleset_fd);
/* Same tests and results as above. */
@@ -1365,7 +1365,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, 0);
enforce_ruleset(_metadata, ruleset_fd);
ASSERT_EQ(0, close(ruleset_fd));
@@ -1417,7 +1417,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, 0);
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
@@ -3970,7 +3970,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
@@ -3985,13 +3985,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;
}
@@ -4789,6 +4790,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";
@@ -7764,4 +7766,2432 @@ 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 ? LANDLOCK_ADD_RULE_QUIET : 0);
+ }
+
+ 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. */
+ EXPECT_EQ(0, 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\\+"));
+
+ /* Check that we see no other logs. */
+ EXPECT_EQ(0, 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 are 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 say.
+ */
+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 say.
+ */
+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 are 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 are 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 are 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, quiet_two_layers_different_handled_1) {
+ /* Quiet from layer 1 */
+ .layers = {
+ {
+ .handled_access_fs = FS_R,
+ .quiet_access_fs = FS_R,
+ .rules = {
+ {
+ .path = file1_s1d1,
+ .access = FS_R,
+ .quiet = true,
+ },
+ {
+ .path = file2_s1d1,
+ .access = 0,
+ .quiet = true,
+ },
+ {
+ .path = file1_s1d2,
+ .access = 0,
+ .quiet = true,
+ },
+ {
+ .path = file2_s1d2,
+ .access = FS_R,
+ .quiet = true,
+ },
+ },
+ },
+ {
+ .handled_access_fs = FS_W,
+ .quiet_access_fs = FS_W,
+ .rules = {
+ {
+ .path = file1_s1d1,
+ .access = FS_W,
+ .quiet = false,
+ },
+ /* Nothing for file2_s1d1 */
+ {
+ .path = file1_s1d2,
+ .access = FS_W,
+ .quiet = false,
+ },
+ /* Nothing for file2_s1d2 */
+ },
+ },
+ },
+ .targets = {
+ {
+ .target = file1_s1d1,
+ .open_mode = O_RDWR,
+ .expect_open_success = true,
+ .expect_truncate_success = true,
+ },
+ /* Missing both, youngest layer denies write, not quiet */
+ {
+ .target = file2_s1d1,
+ .open_mode = O_RDWR,
+ .audit_write_blocked = true,
+ },
+ /* Missing read, denied and quieted by layer 1 */
+ {
+ .target = file1_s1d2,
+ .open_mode = O_RDWR,
+ },
+ /* Missing write, denied and not quieted by layer 2 */
+ {
+ .target = file2_s1d2,
+ .open_mode = O_RDWR,
+ .audit_write_blocked = true,
+ },
+ },
+};
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_different_handled_2) {
+ /* Quiet from layer 2 */
+ .layers = {
+ {
+ .handled_access_fs = FS_R,
+ .quiet_access_fs = FS_R,
+ .rules = {
+ {
+ .path = file1_s1d1,
+ .access = FS_R,
+ .quiet = false,
+ },
+ /* Nothing for file2_s1d1 and file1_s1d2 */
+ {
+ .path = file2_s1d2,
+ .access = FS_R,
+ .quiet = false,
+ },
+ },
+ },
+ {
+ .handled_access_fs = FS_W,
+ .quiet_access_fs = FS_W,
+ .rules = {
+ {
+ .path = file1_s1d1,
+ .access = FS_W,
+ .quiet = true,
+ },
+ {
+ .path = file2_s1d1,
+ .access = 0,
+ .quiet = true,
+ },
+ {
+ .path = file1_s1d2,
+ .access = FS_W,
+ .quiet = true,
+ },
+ {
+ .path = file2_s1d2,
+ .access = 0,
+ .quiet = true,
+ },
+ },
+ },
+ },
+ .targets = {
+ {
+ .target = file1_s1d1,
+ .open_mode = O_RDWR,
+ .expect_open_success = true,
+ .expect_truncate_success = true,
+ },
+ /* Missing both, youngest layer denies write, quiet */
+ {
+ .target = file2_s1d1,
+ .open_mode = O_RDWR,
+ },
+ /* Missing read, denied and not quieted by layer 1 */
+ {
+ .target = file1_s1d2,
+ .open_mode = O_RDWR,
+ .audit_read_blocked = true,
+ },
+ /* Missing write, denied and quieted by layer 2 */
+ {
+ .target = file2_s1d2,
+ .open_mode = O_RDWR,
+ },
+ },
+};
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_different_handled_3) {
+ /* Quiet from both layers */
+ .layers = {
+ {
+ .handled_access_fs = FS_R,
+ .quiet_access_fs = FS_R,
+ .rules = {
+ {
+ .path = file1_s1d1,
+ .access = FS_R,
+ .quiet = true,
+ },
+ {
+ .path = file2_s1d1,
+ .access = 0,
+ .quiet = true,
+ },
+ {
+ .path = file1_s1d2,
+ .access = 0,
+ .quiet = true,
+ },
+ {
+ .path = file2_s1d2,
+ .access = FS_R,
+ .quiet = true,
+ },
+ },
+ },
+ {
+ .handled_access_fs = FS_W,
+ .quiet_access_fs = FS_W,
+ .rules = {
+ {
+ .path = file1_s1d1,
+ .access = FS_W,
+ .quiet = true,
+ },
+ {
+ .path = file2_s1d1,
+ .access = 0,
+ .quiet = true,
+ },
+ {
+ .path = file1_s1d2,
+ .access = FS_W,
+ .quiet = true,
+ },
+ {
+ .path = file2_s1d2,
+ .access = 0,
+ .quiet = true,
+ },
+ },
+ },
+ },
+ .targets = {
+ {
+ .target = file1_s1d1,
+ .open_mode = O_RDWR,
+ .expect_open_success = true,
+ .expect_truncate_success = true,
+ },
+ {
+ .target = file2_s1d1,
+ .open_mode = O_RDWR,
+ },
+ {
+ .target = file1_s1d2,
+ .open_mode = O_RDWR,
+ },
+ {
+ .target = file2_s1d2,
+ .open_mode = O_RDWR,
+ },
+ },
+};
+
+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.
+ */
+ EXPECT_EQ(0, 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 */
+ EXPECT_EQ(0, 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 */
+ EXPECT_EQ(0, 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 */
+ EXPECT_EQ(0, 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);
+
+ EXPECT_EQ(0, 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 */
+ EXPECT_EQ(0, 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 */
+ EXPECT_EQ(0, 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 */
+ EXPECT_EQ(0, 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).
+ */
+ EXPECT_EQ(0, 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.
+ */
+ EXPECT_EQ(0, 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. */
+ EXPECT_EQ(0, 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. */
+ EXPECT_EQ(0, 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);
+
+ EXPECT_EQ(0, 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);
+
+ EXPECT_EQ(0, 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));
+
+ EXPECT_EQ(0, 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);
+
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ ASSERT_EQ(0, records.access);
+}
+
+TEST_F(audit_quiet_rename, quiet_behind_mountpoint_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);
+
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ ASSERT_EQ(0, records.access);
+}
+
TEST_HARNESS_MAIN
--
2.54.0
^ permalink raw reply related
* [PATCH v11 5/9] selftests/landlock: Replace hard-coded 16 with a constant
From: Tingmao Wang @ 2026-06-12 1:48 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1781228815.git.m@maowtm.org>
The next commit will reuse this number. Make it a shared constant to
future-proof changes.
Signed-off-by: Tingmao Wang <m@maowtm.org>
---
Changes in v3:
- 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(-)
diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
index 376931511629..d55fe4c5b531 100644
--- a/tools/testing/selftests/landlock/audit_test.c
+++ b/tools/testing/selftests/landlock/audit_test.c
@@ -79,7 +79,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;
diff --git a/tools/testing/selftests/landlock/common.h b/tools/testing/selftests/landlock/common.h
index 90551650299c..7206d5105d66 100644
--- a/tools/testing/selftests/landlock/common.h
+++ b/tools/testing/selftests/landlock/common.h
@@ -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";
diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index cdb47fc1fc0a..10d9355ade5f 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -1441,7 +1441,7 @@ TEST_F_FORK(layout0, max_layers)
};
const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
- 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++) {
--
2.54.0
^ permalink raw reply related
* [PATCH v11 4/9] samples/landlock: Add quiet flag support to sandboxer
From: Tingmao Wang @ 2026-06-12 1:48 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1781228815.git.m@maowtm.org>
Adds 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.
Assisted-by: GitHub-Copilot:claude-opus-4.8 copilot-reviepickw
Signed-off-by: Tingmao Wang <m@maowtm.org>
---
Changes in v11:
- Error if quiet flags not supported by current kernel but quiet envs provided
- Fix comment
- Refactor env vars in sandboxer: LL_{FS,NET,SCOPED}_QUIET_ACCESS are
merged into one LL_QUIET_ACCESS, and used more sensible names.
Changes in v10:
- Remove stray __attribute__((fallthrough)); (Thanks Justin for
spotting)
Changes in v9:
- Add udp connect / bind quiet flag support
Changes in v8:
- Rebase on top of mic/next
- populate_ruleset_net() already does not require the env var to be
present, so remove redundant comment and check above
populate_ruleset_net(ENV_NET_QUIET_NAME, ...).
Changes in v6:
- Make populate_ruleset_{fs,net} take a flags argument instead of a bool
quiet (suggested by Justin Suess)
- Fix if braces style
Changes in v3:
- Minor change to the above commit message.
Changes in v2:
- 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 | 138 ++++++++++++++++++++++++++++++++---
1 file changed, 127 insertions(+), 11 deletions(-)
diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
index f44db2857bbf..f18228ccf66a 100644
--- a/samples/landlock/sandboxer.c
+++ b/samples/landlock/sandboxer.c
@@ -58,9 +58,12 @@ 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_TCP_BIND_NAME "LL_TCP_BIND"
#define ENV_TCP_CONNECT_NAME "LL_TCP_CONNECT"
+#define ENV_NET_QUIET_NAME "LL_NET_QUIET"
#define ENV_SCOPED_NAME "LL_SCOPED"
+#define ENV_QUIET_ACCESS_NAME "LL_QUIET_ACCESS"
#define ENV_FORCE_LOG_NAME "LL_FORCE_LOG"
#define ENV_UDP_BIND_NAME "LL_UDP_BIND"
#define ENV_UDP_CONNECT_SEND_NAME "LL_UDP_CONNECT_SEND"
@@ -119,7 +122,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, __u32 flags)
{
int num_paths, i, ret = 1;
char *env_path_name;
@@ -169,7 +172,7 @@ 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, flags)) {
fprintf(stderr,
"Failed to update the ruleset with \"%s\": %s\n",
path_list[i], strerror(errno));
@@ -187,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, __u32 flags)
{
int ret = 1;
char *env_port_name, *env_port_name_next, *strport;
@@ -215,7 +218,7 @@ 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, flags)) {
fprintf(stderr,
"Failed to update the ruleset with port \"%llu\": %s\n",
net_port.port, strerror(errno));
@@ -303,6 +306,69 @@ static bool check_ruleset_scope(const char *const env_var,
/* clang-format on */
+/*
+ * Parses ENV_QUIET_ACCESS_NAME and sets the quiet_access_fs,
+ * quiet_access_net and quiet_scoped masks of @ruleset_attr accordingly.
+ */
+static int add_quiet_access(const char *const env_var,
+ struct landlock_ruleset_attr *const ruleset_attr)
+{
+ char *env_quiet_access, *env_quiet_access_next, *str_access;
+
+ 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);
+
+ while ((str_access = strsep(&env_quiet_access_next, ENV_DELIMITER))) {
+ if (strcmp(str_access, "") == 0)
+ continue;
+ else if (strcmp(str_access, "all") == 0) {
+ ruleset_attr->quiet_access_fs =
+ ruleset_attr->handled_access_fs;
+ ruleset_attr->quiet_access_net =
+ ruleset_attr->handled_access_net;
+ ruleset_attr->quiet_scoped = ruleset_attr->scoped;
+ } else if (strcmp(str_access, "read") == 0)
+ ruleset_attr->quiet_access_fs |= ACCESS_FS_ROUGHLY_READ;
+ else if (strcmp(str_access, "write") == 0)
+ ruleset_attr->quiet_access_fs |=
+ ACCESS_FS_ROUGHLY_WRITE;
+ else if (strcmp(str_access, "tcp_bind") == 0)
+ ruleset_attr->quiet_access_net |=
+ LANDLOCK_ACCESS_NET_BIND_TCP;
+ else if (strcmp(str_access, "tcp_connect") == 0)
+ ruleset_attr->quiet_access_net |=
+ LANDLOCK_ACCESS_NET_CONNECT_TCP;
+ else if (strcmp(str_access, "udp_bind") == 0)
+ ruleset_attr->quiet_access_net |=
+ LANDLOCK_ACCESS_NET_BIND_UDP;
+ else if (strcmp(str_access, "udp_connect") == 0)
+ ruleset_attr->quiet_access_net |=
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP;
+ else if (strcmp(str_access, "abstract_unix_socket") == 0)
+ ruleset_attr->quiet_scoped |=
+ LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET;
+ else if (strcmp(str_access, "signal") == 0)
+ ruleset_attr->quiet_scoped |= LANDLOCK_SCOPE_SIGNAL;
+ else {
+ fprintf(stderr, "Unknown quiet access \"%s\"\n",
+ str_access);
+ free(env_quiet_access);
+ return -1;
+ }
+ }
+
+ free(env_quiet_access);
+ ruleset_attr->quiet_access_fs &= ruleset_attr->handled_access_fs;
+ ruleset_attr->quiet_access_net &= ruleset_attr->handled_access_net;
+ ruleset_attr->quiet_scoped &= ruleset_attr->scoped;
+ return 0;
+}
+
#define LANDLOCK_ABI_LAST 10
#define XSTR(s) #s
@@ -337,6 +403,19 @@ 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_QUIET_ACCESS_NAME " can be used to specify which accesses should be quieted "
+ "(required when " ENV_FS_QUIET_NAME " or " ENV_NET_QUIET_NAME " is set):\n"
+ " - \"all\" to quiet all of the accesses below\n"
+ " - \"read\" to quiet all file/dir read accesses\n"
+ " - \"write\" to quiet all file/dir write accesses\n"
+ " - \"tcp_bind\" to quiet tcp bind denials\n"
+ " - \"tcp_connect\" to quiet tcp connect denials\n"
+ " - \"udp_bind\" to quiet udp bind denials\n"
+ " - \"udp_connect\" to quiet udp connect / send denials\n"
+ " - \"abstract_unix_socket\" to quiet abstract unix socket denials\n"
+ " - \"signal\" to quiet signal denials\n"
"\n"
"Example:\n"
ENV_FS_RO_NAME "=\"${PATH}:/lib:/usr:/proc:/etc:/dev/urandom\" "
@@ -369,7 +448,11 @@ int main(const int argc, char *const argv[], char *const *const envp)
LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
.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;
@@ -460,6 +543,8 @@ int main(const int argc, char *const argv[], char *const *const envp)
ruleset_attr.handled_access_net &=
~(LANDLOCK_ACCESS_NET_BIND_UDP |
LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
+ /* Removes quiet flags for ABI < 10 later on. */
+ quiet_supported = false;
/* Must be printed for any ABI < LANDLOCK_ABI_LAST. */
fprintf(stderr,
@@ -526,6 +611,25 @@ int main(const int argc, char *const argv[], char *const *const envp)
unsetenv(ENV_FORCE_LOG_NAME);
}
+ /* Set the quiet access masks. */
+ if (quiet_supported) {
+ if ((getenv(ENV_FS_QUIET_NAME) || getenv(ENV_NET_QUIET_NAME)) &&
+ !getenv(ENV_QUIET_ACCESS_NAME)) {
+ fprintf(stderr,
+ "%s must be set (e.g. to \"all\") when %s or %s is used\n",
+ ENV_QUIET_ACCESS_NAME, ENV_FS_QUIET_NAME,
+ ENV_NET_QUIET_NAME);
+ return 1;
+ }
+ if (add_quiet_access(ENV_QUIET_ACCESS_NAME, &ruleset_attr))
+ return 1;
+ } else if (getenv(ENV_FS_QUIET_NAME) || getenv(ENV_NET_QUIET_NAME) ||
+ getenv(ENV_QUIET_ACCESS_NAME)) {
+ fprintf(stderr,
+ "Quiet flags not supported by current kernel\n");
+ return 1;
+ }
+
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0) {
@@ -533,30 +637,42 @@ 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, 0))
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, 0))
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,
+ LANDLOCK_ADD_RULE_QUIET))
+ goto err_close_ruleset;
}
if (populate_ruleset_net(ENV_TCP_BIND_NAME, ruleset_fd,
- LANDLOCK_ACCESS_NET_BIND_TCP)) {
+ LANDLOCK_ACCESS_NET_BIND_TCP, 0)) {
goto err_close_ruleset;
}
if (populate_ruleset_net(ENV_TCP_CONNECT_NAME, ruleset_fd,
- LANDLOCK_ACCESS_NET_CONNECT_TCP)) {
+ LANDLOCK_ACCESS_NET_CONNECT_TCP, 0)) {
goto err_close_ruleset;
}
if (populate_ruleset_net(ENV_UDP_BIND_NAME, ruleset_fd,
- LANDLOCK_ACCESS_NET_BIND_UDP)) {
+ LANDLOCK_ACCESS_NET_BIND_UDP, 0)) {
goto err_close_ruleset;
}
if (populate_ruleset_net(ENV_UDP_CONNECT_SEND_NAME, ruleset_fd,
- LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP)) {
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, 0)) {
goto err_close_ruleset;
}
+ if (quiet_supported) {
+ if (populate_ruleset_net(ENV_NET_QUIET_NAME, ruleset_fd, 0,
+ LANDLOCK_ADD_RULE_QUIET)) {
+ goto err_close_ruleset;
+ }
+ }
+
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
perror("Failed to restrict privileges");
goto err_close_ruleset;
--
2.54.0
^ permalink raw reply related
* [PATCH v11 3/9] landlock: Suppress logging when quiet flag is present
From: Tingmao Wang @ 2026-06-12 1:48 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1781228815.git.m@maowtm.org>
The 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.
Assisted-by: GitHub-Copilot:claude-opus-4.8 copilot-review
Signed-off-by: Tingmao Wang <m@maowtm.org>
---
Changes in v11:
- Add quiet_optional_accesses invariant check in is_valid_request
- Enhance docstring on landlock_get_quiet_optional_accesses
- Don't use bitfields for fown_layer and quiet_optional_accesses
- Also remove the newly added build_check_file_security() and use
bits_per_field based check for quiet_optional_accesses since we now
don't have a bitfield
Changes in v10:
- clang-format header file changes too
- Fix grammar in some comments
Changes in v9:
- Fix conflict
- Applied struct layer_masks changes to this as well.
- Replace 4 with HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1) in
landlock_get_quiet_optional_accesses()
- Replace 4 with HWEIGHT in (existing) get_layer_from_deny_masks as
well.
- Use optional_access_t typedef for all quiet_optional_accesses values
instead of u8
Changes in v8:
- Rebase on top of mic/next
- Populate request.rule_flags in hook_unix_find()
Changes in v7:
- Following change in commit 1, now we need to copy rule_flags into
landlock_request before calling landlock_log_denial for relevant fs
denials
- Remove left over param comment
Changes in v5:
- Update code style and comment in get_layer_from_deny_masks() and
landlock_log_denial()
- Now that rule_flags is moved into landlock_request, this version removes
the extra parameter for landlock_log_denial and gets rid of
no_rule_flags, simplifying some code.
- Fix build failure without CONFIG_AUDIT (reported by Justin Suess)
Changes in v3:
- 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 in v2:
- 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 | 268 ++++++++++++++++++++++++++++++++++---
security/landlock/audit.h | 1 +
security/landlock/domain.c | 38 ++++++
security/landlock/domain.h | 4 +
security/landlock/fs.c | 6 +
security/landlock/fs.h | 19 ++-
security/landlock/net.c | 15 +--
8 files changed, 330 insertions(+), 26 deletions(-)
diff --git a/security/landlock/access.h b/security/landlock/access.h
index 94f4b9fb7238..e81164876c7d 100644
--- a/security/landlock/access.h
+++ b/security/landlock/access.h
@@ -143,4 +143,9 @@ static inline bool access_mask_subset(access_mask_t subset,
return (subset | superset) == superset;
}
+/* 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 */
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index 8c56f7f6467a..738d8c810b2c 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -249,7 +249,9 @@ static void test_get_denied_layer(struct kunit *const test)
static size_t
get_layer_from_deny_masks(access_mask_t *const access_request,
const access_mask_t all_existing_optional_access,
- const deny_masks_t deny_masks)
+ const deny_masks_t deny_masks,
+ optional_access_t quiet_optional_accesses,
+ bool *quiet)
{
const unsigned long access_opt = all_existing_optional_access;
const unsigned long access_req = *access_request;
@@ -257,6 +259,7 @@ get_layer_from_deny_masks(access_mask_t *const access_request,
size_t youngest_layer = 0;
size_t access_index = 0;
unsigned long access_bit;
+ bool should_quiet = false;
/* This will require change with new object types. */
WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL);
@@ -265,20 +268,33 @@ get_layer_from_deny_masks(access_mask_t *const access_request,
BITS_PER_TYPE(access_mask_t)) {
if (access_req & BIT(access_bit)) {
const size_t layer =
- (deny_masks >> (access_index * 4)) &
+ (deny_masks >>
+ (access_index *
+ HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1))) &
(LANDLOCK_MAX_NUM_LAYERS - 1);
+ const bool layer_has_quiet =
+ !!(quiet_optional_accesses & BIT(access_index));
if (layer > youngest_layer) {
youngest_layer = layer;
missing = BIT(access_bit);
+ should_quiet = layer_has_quiet;
} else if (layer == youngest_layer) {
missing |= BIT(access_bit);
+ /*
+ * Whether the layer has rules with quiet flag covering
+ * the file accessed does not depend on the access, and so
+ * the following WARN_ON_ONCE() should not fail.
+ */
+ WARN_ON_ONCE(should_quiet && !layer_has_quiet);
+ should_quiet = layer_has_quiet;
}
}
access_index++;
}
*access_request = missing;
+ *quiet = should_quiet;
return youngest_layer;
}
@@ -288,42 +304,188 @@ static void test_get_layer_from_deny_masks(struct kunit *const test)
{
deny_masks_t deny_mask;
access_mask_t access;
+ optional_access_t quiet_optional_accesses;
+ bool quiet;
/* truncate:0 ioctl_dev:2 */
deny_mask = 0x20;
+ quiet_optional_accesses = 0;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 0,
- get_layer_from_deny_masks(&access,
- _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask));
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, false);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 2,
- get_layer_from_deny_masks(&access,
- _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask));
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ /* layer denying truncate: quiet, ioctl: not quiet */
+ quiet_optional_accesses = 0b01;
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE;
+ KUNIT_EXPECT_EQ(test, 0,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, true);
+
+ access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ /* Reverse order - truncate:2 ioctl_dev:0 */
+ deny_mask = 0x02;
+ quiet_optional_accesses = 0;
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 0,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ /* layer denying truncate: quiet, ioctl: not quiet */
+ quiet_optional_accesses = 0b01;
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, true);
+
+ access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 0,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, true);
+
+ /* layer denying truncate: not quiet, ioctl: quiet */
+ quiet_optional_accesses = 0b10;
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 0,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, true);
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, false);
/* truncate:15 ioctl_dev:15 */
deny_mask = 0xff;
+ quiet_optional_accesses = 0;
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE;
+ KUNIT_EXPECT_EQ(test, 15,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 15,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access,
+ LANDLOCK_ACCESS_FS_TRUNCATE |
+ LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ /* Both quiet (same layer so quietness must be the same) */
+ quiet_optional_accesses = 0b11;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 15,
- get_layer_from_deny_masks(&access,
- _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask));
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, true);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 15,
- get_layer_from_deny_masks(&access,
- _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask));
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access,
LANDLOCK_ACCESS_FS_TRUNCATE |
LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, true);
}
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
@@ -349,11 +511,34 @@ static bool is_valid_request(const struct landlock_request *const request)
if (request->deny_masks) {
if (WARN_ON_ONCE(!request->all_existing_optional_access))
return false;
+ static_assert(sizeof(request->all_existing_optional_access) ==
+ sizeof(u32));
+ if (WARN_ON_ONCE(
+ request->quiet_optional_accesses >=
+ BIT(hweight32(
+ request->all_existing_optional_access))))
+ return false;
}
return true;
}
+static access_mask_t
+pick_access_mask_for_request_type(const enum landlock_request_type type,
+ const struct access_masks access_masks)
+{
+ switch (type) {
+ case LANDLOCK_REQUEST_FS_ACCESS:
+ return access_masks.fs;
+ case LANDLOCK_REQUEST_NET_ACCESS:
+ return access_masks.net;
+ default:
+ WARN_ONCE(1, "Invalid request type %d passed to %s", type,
+ __func__);
+ return 0;
+ }
+}
+
/**
* landlock_log_denial - Create audit records related to a denial
*
@@ -367,6 +552,7 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
struct landlock_hierarchy *youngest_denied;
size_t youngest_layer;
access_mask_t missing;
+ bool object_quiet_flag = false, quiet_applicable_to_access = false;
if (WARN_ON_ONCE(!subject || !subject->domain ||
!subject->domain->hierarchy || !request))
@@ -382,10 +568,15 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
youngest_layer = get_denied_layer(subject->domain,
&missing,
request->layer_masks);
+ object_quiet_flag =
+ request->layer_masks->layers[youngest_layer]
+ .quiet;
} else {
youngest_layer = get_layer_from_deny_masks(
&missing, _LANDLOCK_ACCESS_FS_OPTIONAL,
- request->deny_masks);
+ request->deny_masks,
+ request->quiet_optional_accesses,
+ &object_quiet_flag);
}
youngest_denied =
get_hierarchy(subject->domain, youngest_layer);
@@ -420,6 +611,53 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
return;
}
+ /*
+ * Checks if the object is marked quiet by the layer that denied the
+ * request. If it's a different layer that marked it as quiet, but
+ * that layer is not the one that denied the request, we should still
+ * audit log the denial.
+ */
+ if (object_quiet_flag) {
+ /*
+ * We now check if the denied requests are all covered by the
+ * layer's quiet access bits.
+ */
+ const access_mask_t quiet_mask =
+ pick_access_mask_for_request_type(
+ request->type, youngest_denied->quiet_masks);
+
+ quiet_applicable_to_access = (quiet_mask & missing) == missing;
+ } else {
+ /*
+ * Either the object is not quiet, or this is a scope request. We
+ * check request->type to distinguish between the two cases.
+ */
+ const access_mask_t quiet_mask =
+ youngest_denied->quiet_masks.scope;
+
+ switch (request->type) {
+ case LANDLOCK_REQUEST_SCOPE_SIGNAL:
+ quiet_applicable_to_access =
+ !!(quiet_mask & LANDLOCK_SCOPE_SIGNAL);
+ break;
+ case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
+ quiet_applicable_to_access =
+ !!(quiet_mask &
+ LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
+ break;
+ /*
+ * Leave LANDLOCK_REQUEST_PTRACE and
+ * LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY unhandled for now - they are
+ * never quiet.
+ */
+ default:
+ break;
+ }
+ }
+
+ if (quiet_applicable_to_access)
+ return;
+
/* Uses consistent allocation flags wrt common_lsm_audit(). */
ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN,
AUDIT_LANDLOCK_ACCESS);
diff --git a/security/landlock/audit.h b/security/landlock/audit.h
index b85d752273ac..620f8a24291d 100644
--- a/security/landlock/audit.h
+++ b/security/landlock/audit.h
@@ -48,6 +48,7 @@ struct landlock_request {
/* Required fields for requests with deny masks. */
const access_mask_t all_existing_optional_access;
deny_masks_t deny_masks;
+ optional_access_t quiet_optional_accesses;
};
#ifdef CONFIG_AUDIT
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index d1a4d8b33ee1..6f1cff739ae8 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -157,6 +157,44 @@ get_layer_deny_mask(const access_mask_t all_existing_optional_access,
<< ((access_weight - 1) * HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1));
}
+/**
+ * landlock_get_quiet_optional_accesses - Get optional accesses which are
+ * covered by quiet rule flags.
+ *
+ * @all_existing_optional_access: Bitmask of valid optional accesses.
+ * @deny_masks: Domain layer levels that denied each optional access (the
+ * deny_masks field on struct landlock_file_security).
+ * @masks: The struct layer_masks collected during the path walk.
+ *
+ * Return: a bitmask of which optional accesses are denied by layers for
+ * which the quiet flag was collected during the path walk.
+ */
+optional_access_t landlock_get_quiet_optional_accesses(
+ const access_mask_t all_existing_optional_access,
+ const deny_masks_t deny_masks, const struct layer_masks *const masks)
+{
+ 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 *
+ HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1))) &
+ (LANDLOCK_MAX_NUM_LAYERS - 1);
+
+ if (masks->layers[layer].quiet)
+ quiet_optional_accesses |= BIT(access_index);
+ access_index++;
+ }
+ return quiet_optional_accesses;
+}
+
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static void test_get_layer_deny_mask(struct kunit *const test)
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index 9f560f3c3bd1..2a1660e3dea7 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -126,6 +126,10 @@ landlock_get_deny_masks(const access_mask_t all_existing_optional_access,
const access_mask_t optional_access,
const struct layer_masks *const masks);
+optional_access_t landlock_get_quiet_optional_accesses(
+ const access_mask_t all_existing_optional_access,
+ const deny_masks_t deny_masks, const struct layer_masks *const masks);
+
int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy);
static inline void
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index a096e4aa7fcd..ccb2fe4fa056 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -1805,6 +1805,10 @@ static int hook_file_open(struct file *const file)
#ifdef CONFIG_AUDIT
landlock_file(file)->deny_masks = landlock_get_deny_masks(
_LANDLOCK_ACCESS_FS_OPTIONAL, optional_access, &layer_masks);
+ landlock_file(file)->quiet_optional_accesses =
+ landlock_get_quiet_optional_accesses(
+ _LANDLOCK_ACCESS_FS_OPTIONAL,
+ landlock_file(file)->deny_masks, &layer_masks);
#endif /* CONFIG_AUDIT */
if (access_mask_subset(open_access_request, allowed_access))
@@ -1841,6 +1845,7 @@ static int hook_file_truncate(struct file *const file)
.access = LANDLOCK_ACCESS_FS_TRUNCATE,
#ifdef CONFIG_AUDIT
.deny_masks = landlock_file(file)->deny_masks,
+ .quiet_optional_accesses = landlock_file(file)->quiet_optional_accesses,
#endif /* CONFIG_AUDIT */
});
return -EACCES;
@@ -1880,6 +1885,7 @@ static int hook_file_ioctl_common(const struct file *const file,
.access = LANDLOCK_ACCESS_FS_IOCTL_DEV,
#ifdef CONFIG_AUDIT
.deny_masks = landlock_file(file)->deny_masks,
+ .quiet_optional_accesses = landlock_file(file)->quiet_optional_accesses,
#endif /* CONFIG_AUDIT */
});
return -EACCES;
diff --git a/security/landlock/fs.h b/security/landlock/fs.h
index e4c530511360..7efe9b172acf 100644
--- a/security/landlock/fs.h
+++ b/security/landlock/fs.h
@@ -63,6 +63,14 @@ 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;
/**
* @fown_layer: Layer level of @fown_subject->domain with
* LANDLOCK_SCOPE_SIGNAL.
@@ -91,13 +99,18 @@ 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 */
-
+/* clang-format on */
+/*
+ * Make sure quiet_optional_accesses has enough bits to cover all optional
+ * accesses.
+ */
+static_assert(BITS_PER_TYPE(typeof_member(struct landlock_file_security,
+ quiet_optional_accesses)) >=
+ HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL));
#endif /* CONFIG_AUDIT */
/**
diff --git a/security/landlock/net.c b/security/landlock/net.c
index 4b4f974dc877..51050dd39a3a 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -250,14 +250,13 @@ static int current_check_access_socket(struct socket *const sock,
audit_net.family = address->sa_family;
audit_net.sk = sock->sk;
- landlock_log_denial(subject,
- &(struct landlock_request){
- .type = LANDLOCK_REQUEST_NET_ACCESS,
- .audit.type = LSM_AUDIT_DATA_NET,
- .audit.u.net = &audit_net,
- .access = access_request,
- .layer_masks = &layer_masks,
- });
+ landlock_log_denial(
+ subject,
+ &(struct landlock_request){ .type = LANDLOCK_REQUEST_NET_ACCESS,
+ .audit.type = LSM_AUDIT_DATA_NET,
+ .audit.u.net = &audit_net,
+ .access = access_request,
+ .layer_masks = &layer_masks });
return -EACCES;
}
--
2.54.0
^ permalink raw reply related
* [PATCH v11 2/9] landlock: Add API support and docs for the quiet flags
From: Tingmao Wang @ 2026-06-12 1:48 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1781228815.git.m@maowtm.org>
Adds the UAPI for the quiet flags feature (but not the implementation
yet).
Even though currently LANDLOCK_ADD_RULE_QUIET only affects audit
logging, in the future this can also be used as part of a supervisor
mechanism, where it will also suppress denial notifications on a
per-object basis. Thus the name is deliberately generic, as opposed to
e.g. LANDLOCK_ADD_RULE_LOG_QUIET.
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.
Assisted-by: GitHub-Copilot:claude-opus-4.8
Signed-off-by: Tingmao Wang <m@maowtm.org>
---
Changes in v11:
- Fix doc: clarify that one landlock_add_rule() call with the quiet flag
is enough to mark the object as quiet.
- audit log -> log
- Change all newly added flags argument to u32 to match syscall argument.
Changes in v9:
- Move a mistakenly included hunk into patch 1
- Doc change for sys_landlock_create_ruleset to add missing
"quiet_scoped | scoped == scoped" requirement.
- Doc changes for struct landlock_ruleset_attr, and re-wrap added bits
wider to stay consistent with the existing block.
- Other style changes from suggestions
- Added mention of this flag in the audit section of
Documentation/admin-guide/LSM/landlock.rst
- Added a block for this new flag to the "Previous limitations" section
in Documentation/userspace-api/landlock.rst
Changes in v8:
- The new Landlock ABI version is now v10 as a result of rebase.
- Allocate a rule_flags in hook_unix_find() and pass to
is_access_to_paths_allowed().
Changes in v6:
- Fix typo in doc
Changes in v5:
- Doc fixes.
- Fix build failure without CONFIG_AUDIT / CONFIG_INET (reported by Justin
Suess)
Changes in v4:
- Minor update to this commit message.
- Fix minor formatting
Changes in v3:
- Updated docs from Mickaël's suggestions.
Changes in v2:
- 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.
Documentation/admin-guide/LSM/landlock.rst | 9 ++-
Documentation/userspace-api/landlock.rst | 14 ++++
include/uapi/linux/landlock.h | 61 +++++++++++++++++
security/landlock/domain.h | 5 ++
security/landlock/fs.c | 4 +-
security/landlock/fs.h | 2 +-
security/landlock/net.c | 5 +-
security/landlock/net.h | 5 +-
security/landlock/ruleset.c | 12 +++-
security/landlock/ruleset.h | 12 +++-
security/landlock/syscalls.c | 71 +++++++++++++++-----
tools/testing/selftests/landlock/base_test.c | 2 +-
12 files changed, 171 insertions(+), 31 deletions(-)
diff --git a/Documentation/admin-guide/LSM/landlock.rst b/Documentation/admin-guide/LSM/landlock.rst
index 9923874e2156..ccc32dad1d1c 100644
--- a/Documentation/admin-guide/LSM/landlock.rst
+++ b/Documentation/admin-guide/LSM/landlock.rst
@@ -19,8 +19,10 @@ Audit
Denied access requests are logged by default for a sandboxed program if `audit`
is enabled. This default behavior can be changed with the
sys_landlock_restrict_self() flags (cf.
-Documentation/userspace-api/landlock.rst). Landlock logs can also be masked
-thanks to audit rules. Landlock can generate 2 audit record types.
+Documentation/userspace-api/landlock.rst), or suppressed on a per-object
+basis by using ``LANDLOCK_ADD_RULE_QUIET`` (ABI 10+). Landlock logs can
+also be masked thanks to audit rules. Landlock can generate 2 audit
+record types.
Record types
------------
@@ -172,7 +174,8 @@ If you get spammed with audit logs related to Landlock, this is either an
attack attempt or a bug in the security policy. We can put in place some
filters to limit noise with two complementary ways:
-- with sys_landlock_restrict_self()'s flags if we can fix the sandboxed
+- with sys_landlock_restrict_self()'s flags, or
+ ``LANDLOCK_ADD_RULE_QUIET`` (ABI 10+) if we can fix the sandboxed
programs,
- or with audit rules (see :manpage:`auditctl(8)`).
diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
index 0ea55c2c732c..ce63ec564229 100644
--- a/Documentation/userspace-api/landlock.rst
+++ b/Documentation/userspace-api/landlock.rst
@@ -775,6 +775,20 @@ remote port of UDP sockets (via :manpage:`connect(2)), and sending
datagrams to an explicit remote port (ignoring any destination set on
UDP sockets, via e.g. :manpage:`sendto(2)).
+Quiet rule flag (ABI < 10)
+--------------------------
+
+Starting with the Landlock ABI version 10, it is possible to selectively
+suppress logs for specific denied accesses on a per-object basis with
+the ``LANDLOCK_ADD_RULE_QUIET`` flag of sys_landlock_add_rule(), in
+combination with the ``quiet_access_fs`` and ``quiet_access_net`` fields
+of struct landlock_ruleset_attr. It is also now possible to suppress
+logs for scope accesses via the ``quiet_scoped`` field of struct
+landlock_ruleset_attr. The object is marked as quiet within a ruleset
+when at least one sys_landlock_add_rule() call is made for it with the
+``LANDLOCK_ADD_RULE_QUIET`` flag, additional add-rule calls for the same
+object without this flag do not clear it.
+
.. _kernel_support:
Kernel support
diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index b147223efc97..1bdd9444335f 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -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 a 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
+ * 10.
+ *
+ * @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,21 @@ struct landlock_ruleset_attr {
* resources (e.g. IPCs).
*/
__u64 scoped;
+ /**
+ * @quiet_access_fs: Bitmask of filesystem actions which should not be
+ * logged if per-object quiet flag is set.
+ */
+ __u64 quiet_access_fs;
+ /**
+ * @quiet_access_net: Bitmask of network actions which should not be
+ * logged if per-object quiet flag is set.
+ */
+ __u64 quiet_access_net;
+ /**
+ * @quiet_scoped: Bitmask of scoped actions which should not be
+ * logged.
+ */
+ __u64 quiet_scoped;
};
/**
@@ -69,6 +97,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 logging is enabled, when Landlock denies an access, it will
+ * suppress the log if all of the following are true:
+ *
+ * - this layer is the innermost layer that denied the access;
+ * - all accesses denied by this layer 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
*
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index af100a8cd939..9f560f3c3bd1 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -111,6 +111,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 */
};
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index c724692bb990..a096e4aa7fcd 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -325,7 +325,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 u32 flags)
{
int err;
struct landlock_id id = {
@@ -346,7 +346,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()
diff --git a/security/landlock/fs.h b/security/landlock/fs.h
index 911b83669e20..e4c530511360 100644
--- a/security/landlock/fs.h
+++ b/security/landlock/fs.h
@@ -136,6 +136,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 u32 flags);
#endif /* _SECURITY_LANDLOCK_FS_H */
diff --git a/security/landlock/net.c b/security/landlock/net.c
index d472e6cab12f..4b4f974dc877 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -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 u32 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;
diff --git a/security/landlock/net.h b/security/landlock/net.h
index 09960c237a13..5c0e3b4090cb 100644
--- a/security/landlock/net.h
+++ b/security/landlock/net.h
@@ -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 u32 flags);
#else /* IS_ENABLED(CONFIG_INET) */
static inline void landlock_add_net_hooks(void)
{
@@ -24,7 +25,7 @@ static inline void landlock_add_net_hooks(void)
static inline int
landlock_append_net_rule(struct landlock_ruleset *const ruleset, const u16 port,
- access_mask_t access_rights)
+ access_mask_t access_rights, const u32 flags)
{
return -EAFNOSUPPORT;
}
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 91948e406e69..46cda04d9670 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -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 "domain.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 u32 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,10 @@ landlock_merge_ruleset(struct landlock_ruleset *const parent,
if (err)
return ERR_PTR(err);
+#ifdef CONFIG_AUDIT
+ new_dom->hierarchy->quiet_masks = ruleset->quiet_masks;
+#endif /* CONFIG_AUDIT */
+
return no_free_ptr(new_dom);
}
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index d0fa9af46a2c..c819d0c40796 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -156,8 +156,8 @@ struct landlock_ruleset {
* @work_free: Enables to free a ruleset within a lockless
* section. This is only used by
* landlock_put_ruleset_deferred() when @usage reaches zero.
- * The fields @lock, @usage, @num_rules, @num_layers and
- * @access_masks are then unused.
+ * The fields @lock, @usage, @num_rules, @num_layers, @quiet_masks
+ * and @access_masks are then unused.
*/
struct work_struct work_free;
struct {
@@ -183,6 +183,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.
@@ -213,7 +219,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 u32 flags);
struct landlock_ruleset *
landlock_merge_ruleset(struct landlock_ruleset *const parent,
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index d45469d5d464..425f093e2407 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -105,8 +105,11 @@ static void build_check_abi(void)
ruleset_size = sizeof(ruleset_attr.handled_access_fs);
ruleset_size += sizeof(ruleset_attr.handled_access_net);
ruleset_size += sizeof(ruleset_attr.scoped);
+ ruleset_size += sizeof(ruleset_attr.quiet_access_fs);
+ ruleset_size += sizeof(ruleset_attr.quiet_access_net);
+ ruleset_size += sizeof(ruleset_attr.quiet_scoped);
BUILD_BUG_ON(sizeof(ruleset_attr) != ruleset_size);
- BUILD_BUG_ON(sizeof(ruleset_attr) != 24);
+ BUILD_BUG_ON(sizeof(ruleset_attr) != 48);
path_beneath_size = sizeof(path_beneath_attr.allowed_access);
path_beneath_size += sizeof(path_beneath_attr.parent_fd);
@@ -193,6 +196,9 @@ const int landlock_abi_version = 10;
* - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
* - %EINVAL: unknown @flags, or unknown access, or unknown scope, or too small
* @size;
+ * - %EINVAL: quiet_access_fs, quiet_access_net, or quiet_scoped is not a
+ * subset of the corresponding handled_access_fs, handled_access_net, or
+ * scoped;
* - %E2BIG: @attr or @size inconsistencies;
* - %EFAULT: @attr or @size inconsistencies;
* - %ENOMSG: empty &landlock_ruleset_attr.handled_access_fs.
@@ -249,6 +255,21 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
if ((ruleset_attr.scoped | LANDLOCK_MASK_SCOPE) != LANDLOCK_MASK_SCOPE)
return -EINVAL;
+ /*
+ * Check that quiet masks are subsets of the respective handled masks.
+ * Because of the checks above this is sufficient to also ensure that
+ * the quiet masks are valid access masks.
+ */
+ if ((ruleset_attr.quiet_access_fs | ruleset_attr.handled_access_fs) !=
+ ruleset_attr.handled_access_fs)
+ return -EINVAL;
+ if ((ruleset_attr.quiet_access_net | ruleset_attr.handled_access_net) !=
+ ruleset_attr.handled_access_net)
+ return -EINVAL;
+ if ((ruleset_attr.quiet_scoped | ruleset_attr.scoped) !=
+ ruleset_attr.scoped)
+ return -EINVAL;
+
/* Checks arguments and transforms to kernel struct. */
ruleset = landlock_create_ruleset(ruleset_attr.handled_access_fs,
ruleset_attr.handled_access_net,
@@ -256,6 +277,10 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
if (IS_ERR(ruleset))
return PTR_ERR(ruleset);
+ ruleset->quiet_masks.fs = ruleset_attr.quiet_access_fs;
+ ruleset->quiet_masks.net = ruleset_attr.quiet_access_net;
+ ruleset->quiet_masks.scope = ruleset_attr.quiet_scoped;
+
/* Creates anonymous FD referring to the ruleset. */
ruleset_fd = anon_inode_getfd("[landlock-ruleset]", &ruleset_fops,
ruleset, O_RDWR | O_CLOEXEC);
@@ -320,7 +345,7 @@ static int get_path_from_fd(const s32 fd, struct path *const path)
}
static int add_rule_path_beneath(struct landlock_ruleset *const ruleset,
- const void __user *const rule_attr)
+ const void __user *const rule_attr, u32 flags)
{
struct landlock_path_beneath_attr path_beneath_attr;
struct path path;
@@ -335,9 +360,10 @@ static int add_rule_path_beneath(struct landlock_ruleset *const ruleset,
/*
* Informs about useless rule: empty allowed_access (i.e. deny rules)
- * are ignored in path walks.
+ * are ignored in path walks. However, the rule is not useless if it
+ * is there to hold a quiet flag.
*/
- if (!path_beneath_attr.allowed_access)
+ if (!flags && !path_beneath_attr.allowed_access)
return -ENOMSG;
/* Checks that allowed_access matches the @ruleset constraints. */
@@ -345,6 +371,10 @@ static int add_rule_path_beneath(struct landlock_ruleset *const ruleset,
if ((path_beneath_attr.allowed_access | mask) != mask)
return -EINVAL;
+ /* Checks for useless quiet flag. */
+ if (flags & LANDLOCK_ADD_RULE_QUIET && !ruleset->quiet_masks.fs)
+ return -EINVAL;
+
/* Gets and checks the new rule. */
err = get_path_from_fd(path_beneath_attr.parent_fd, &path);
if (err)
@@ -352,13 +382,13 @@ static int add_rule_path_beneath(struct landlock_ruleset *const ruleset,
/* Imports the new rule. */
err = landlock_append_fs_rule(ruleset, &path,
- path_beneath_attr.allowed_access);
+ path_beneath_attr.allowed_access, flags);
path_put(&path);
return err;
}
static int add_rule_net_port(struct landlock_ruleset *ruleset,
- const void __user *const rule_attr)
+ const void __user *const rule_attr, u32 flags)
{
struct landlock_net_port_attr net_port_attr;
int res;
@@ -371,9 +401,10 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
/*
* Informs about useless rule: empty allowed_access (i.e. deny rules)
- * are ignored by network actions.
+ * are ignored by network actions. However, the rule is not useless
+ * if it is there to hold a quiet flag.
*/
- if (!net_port_attr.allowed_access)
+ if (!flags && !net_port_attr.allowed_access)
return -ENOMSG;
/* Checks that allowed_access matches the @ruleset constraints. */
@@ -381,13 +412,17 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
if ((net_port_attr.allowed_access | mask) != mask)
return -EINVAL;
+ /* Checks for useless quiet flag. */
+ if (flags & LANDLOCK_ADD_RULE_QUIET && !ruleset->quiet_masks.net)
+ return -EINVAL;
+
/* Denies inserting a rule with port greater than 65535. */
if (net_port_attr.port > U16_MAX)
return -EINVAL;
/* Imports the new rule. */
return landlock_append_net_rule(ruleset, net_port_attr.port,
- net_port_attr.allowed_access);
+ net_port_attr.allowed_access, flags);
}
/**
@@ -398,7 +433,7 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
* @rule_type: Identify the structure type pointed to by @rule_attr:
* %LANDLOCK_RULE_PATH_BENEATH or %LANDLOCK_RULE_NET_PORT.
* @rule_attr: Pointer to a rule (matching the @rule_type).
- * @flags: Must be 0.
+ * @flags: Must be 0 or %LANDLOCK_ADD_RULE_QUIET.
*
* This system call enables to define a new rule and add it to an existing
* ruleset.
@@ -408,20 +443,25 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
* - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
* - %EAFNOSUPPORT: @rule_type is %LANDLOCK_RULE_NET_PORT but TCP/IP is not
* supported by the running kernel;
- * - %EINVAL: @flags is not 0;
+ * - %EINVAL: @flags is not valid;
* - %EINVAL: The rule accesses are inconsistent (i.e.
* &landlock_path_beneath_attr.allowed_access or
* &landlock_net_port_attr.allowed_access is not a subset of the ruleset
* handled accesses)
* - %EINVAL: &landlock_net_port_attr.port is greater than 65535;
+ * - %EINVAL: LANDLOCK_ADD_RULE_QUIET is passed but the ruleset has no
+ * quiet access bits set for the corresponding rule type.
* - %ENOMSG: Empty accesses (e.g. &landlock_path_beneath_attr.allowed_access is
- * 0);
+ * 0) and no flags;
* - %EBADF: @ruleset_fd is not a file descriptor for the current thread, or a
* member of @rule_attr is not a file descriptor as expected;
* - %EBADFD: @ruleset_fd is not a ruleset file descriptor, or a member of
* @rule_attr is not the expected file descriptor type;
* - %EPERM: @ruleset_fd has no write access to the underlying ruleset;
* - %EFAULT: @rule_attr was not a valid address.
+ *
+ * .. kernel-doc:: include/uapi/linux/landlock.h
+ * :identifiers: landlock_add_rule_flags
*/
SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
const enum landlock_rule_type, rule_type,
@@ -432,8 +472,7 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
if (!is_initialized())
return -EOPNOTSUPP;
- /* No flag for now. */
- if (flags)
+ if (flags && flags != LANDLOCK_ADD_RULE_QUIET)
return -EINVAL;
/* Gets and checks the ruleset. */
@@ -443,9 +482,9 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
switch (rule_type) {
case LANDLOCK_RULE_PATH_BENEATH:
- return add_rule_path_beneath(ruleset, rule_attr);
+ return add_rule_path_beneath(ruleset, rule_attr, flags);
case LANDLOCK_RULE_NET_PORT:
- return add_rule_net_port(ruleset, rule_attr);
+ return add_rule_net_port(ruleset, rule_attr, flags);
default:
return -EINVAL;
}
diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
index 6c8113c2ded1..84e91fcaa1b2 100644
--- a/tools/testing/selftests/landlock/base_test.c
+++ b/tools/testing/selftests/landlock/base_test.c
@@ -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. */
--
2.54.0
^ permalink raw reply related
* [PATCH v11 1/9] landlock: Add a place for flags to layer rules
From: Tingmao Wang @ 2026-06-12 1:48 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1781228815.git.m@maowtm.org>
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.
struct layer_access_masks is renamed to struct layer_masks, and a new
field is added to track whether a quiet flag rule is seen for each
layer. Through use of bitfields, this does not increase the size of the
struct.
Cc: Justin Suess <utilityemal77@gmail.com>
Assisted-by: GitHub-Copilot:claude-opus-4.8 copilot-review
Signed-off-by: Tingmao Wang <m@maowtm.org>
Co-developed-by: Justin Suess <utilityemal77@gmail.com>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
Tested-by: Justin Suess <utilityemal77@gmail.com>
---
Changes in v11:
- doc changes from feedback
- access_masks_t quiet : 1 instead of bool quiet : 1
- __packed __aligned(sizeof(access_mask_t))
- u8 quiet:1 instead of bool quiet:1 in struct landlock_layer
- Turn all "audit log" mentions into "log"
Changes in v10:
- Doc for struct layer_mask members
- clang-format header file changes
- Add Tested-by for Justin Suess
Changes in v9:
- Move a hunk from patch 2 to here
- Fix comment and format
- Renamed struct layer_access_masks to struct layer_masks, and moved the
content of struct collected_rule_flags into this struct, getting rid
of the extra struct collected_rule_flags and function parameters.
This is following a discussion in [3]. The flag is now initialized in
landlock_init_layer_masks as false.
- Thus also removed now unnecessary layer_mask_t
Changes in v8:
- Rebase on top of mic/next
- Add Co-developed-by: Justin Suess for handling this rebase initially
- layer_mask_t was removed in [1] but we still need it for the
collected_rule_flags. Rather than using raw u16, I've chosen to
re-define it back in ruleset.h (it was in access.h).
Changes in v7:
- Take rule_flags separately from landlock_request in
is_access_to_paths_allowed to avoid writing to the landlock_request
variable if CONFIG_AUDIT is disabled (to enable compiler elision).
- Due to the above change, we don't need rule_flags in landlock_request in
this commit anymore (will be added later).
Changes in v6:
- Rebased to include the revised disconnected directory handling changes
(without the "reverting" behaviour)
Changes in v5:
- Move rule_flags into landlock_request. This lets us get rid of the
extra parameters to is_access_to_paths_allowed (and later on,
landlock_log_denial), and thus less code changes.
Changes in v3:
- Comment changes, move local variables, simplify if branch
Changes in v2:
- Comment changes
- Rebased to include disconnected directory handling changes on mic/next
and add backing up of collected_rule_flags.
[1]: https://lore.kernel.org/all/20260125195853.109967-1-gnoack3000@gmail.com/
[2]: https://lore.kernel.org/all/20251221194301.247484-1-utilityemal77@gmail.com/
[3]: https://lore.kernel.org/all/20260524.eFiz4hahrami@digikod.net/
security/landlock/access.h | 39 ++++++++--
security/landlock/audit.c | 20 ++---
security/landlock/audit.h | 2 +-
security/landlock/domain.c | 19 ++---
security/landlock/domain.h | 2 +-
security/landlock/fs.c | 147 +++++++++++++++++++-----------------
security/landlock/limits.h | 3 +
security/landlock/net.c | 2 +-
security/landlock/ruleset.c | 33 +++++---
security/landlock/ruleset.h | 17 ++++-
10 files changed, 174 insertions(+), 110 deletions(-)
diff --git a/security/landlock/access.h b/security/landlock/access.h
index c19d5bc13944..94f4b9fb7238 100644
--- a/security/landlock/access.h
+++ b/security/landlock/access.h
@@ -62,18 +62,41 @@ static_assert(sizeof(typeof_member(union access_masks_all, masks)) ==
sizeof(typeof_member(union access_masks_all, all)));
/**
- * struct layer_access_masks - A boolean matrix of layers and access rights
+ * struct layer_mask - The access rights and rule flags for a layer.
*
- * This has a bit for each combination of layer numbers and access rights.
- * During access checks, it is used to represent the access rights for each
- * layer which still need to be fulfilled. When all bits are 0, the access
- * request is considered to be fulfilled.
+ * This has a bit for each access rights and rule flags. During access
+ * checks, it is used to represent the access rights for each layer which
+ * still need to be fulfilled. When all bits are 0, the access request is
+ * considered to be fulfilled.
*/
-struct layer_access_masks {
+struct layer_mask {
/**
- * @access: The unfulfilled access rights for each layer.
+ * @access: The unfulfilled access rights for this layer.
*/
- access_mask_t access[LANDLOCK_MAX_NUM_LAYERS];
+ access_mask_t access : LANDLOCK_NUM_ACCESS_MAX;
+#ifdef CONFIG_AUDIT
+ /**
+ * @quiet: Whether we have encountered a rule with the quiet flag for
+ * this layer. Used to control logging.
+ */
+ access_mask_t quiet : 1;
+#endif /* CONFIG_AUDIT */
+} __packed __aligned(sizeof(access_mask_t));
+
+/*
+ * Make sure that we don't increase the size of struct layer_mask when
+ * storing rule flags.
+ */
+static_assert(sizeof(struct layer_mask) == sizeof(access_mask_t));
+
+/**
+ * struct layer_masks - An array of struct layer_mask, one per layer.
+ */
+struct layer_masks {
+ /**
+ * @layers: The unfulfilled access rights for each layer.
+ */
+ struct layer_mask layers[LANDLOCK_MAX_NUM_LAYERS];
};
/*
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index 851647197a01..8c56f7f6467a 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -187,11 +187,11 @@ static void test_get_hierarchy(struct kunit *const test)
/* Get the youngest layer that denied the access_request. */
static size_t get_denied_layer(const struct landlock_ruleset *const domain,
access_mask_t *const access_request,
- const struct layer_access_masks *masks)
+ const struct layer_masks *masks)
{
- for (ssize_t i = ARRAY_SIZE(masks->access) - 1; i >= 0; i--) {
- if (masks->access[i] & *access_request) {
- *access_request &= masks->access[i];
+ for (ssize_t i = ARRAY_SIZE(masks->layers) - 1; i >= 0; i--) {
+ if (masks->layers[i].access & *access_request) {
+ *access_request &= masks->layers[i].access;
return i;
}
}
@@ -208,12 +208,12 @@ static void test_get_denied_layer(struct kunit *const test)
const struct landlock_ruleset dom = {
.num_layers = 5,
};
- const struct layer_access_masks masks = {
- .access[0] = LANDLOCK_ACCESS_FS_EXECUTE |
- LANDLOCK_ACCESS_FS_READ_DIR,
- .access[1] = LANDLOCK_ACCESS_FS_READ_FILE |
- LANDLOCK_ACCESS_FS_READ_DIR,
- .access[2] = LANDLOCK_ACCESS_FS_REMOVE_DIR,
+ const struct layer_masks masks = {
+ .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE |
+ LANDLOCK_ACCESS_FS_READ_DIR,
+ .layers[1].access = LANDLOCK_ACCESS_FS_READ_FILE |
+ LANDLOCK_ACCESS_FS_READ_DIR,
+ .layers[2].access = LANDLOCK_ACCESS_FS_REMOVE_DIR,
};
access_mask_t access;
diff --git a/security/landlock/audit.h b/security/landlock/audit.h
index 56778331b58c..b85d752273ac 100644
--- a/security/landlock/audit.h
+++ b/security/landlock/audit.h
@@ -43,7 +43,7 @@ struct landlock_request {
access_mask_t access;
/* Required fields for requests with layer masks. */
- const struct layer_access_masks *layer_masks;
+ const struct layer_masks *layer_masks;
/* Required fields for requests with deny masks. */
const access_mask_t all_existing_optional_access;
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 5dd06f7c2312..d1a4d8b33ee1 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -184,7 +184,7 @@ static void test_get_layer_deny_mask(struct kunit *const test)
deny_masks_t
landlock_get_deny_masks(const access_mask_t all_existing_optional_access,
const access_mask_t optional_access,
- const struct layer_access_masks *const masks)
+ const struct layer_masks *const masks)
{
const unsigned long access_opt = optional_access;
unsigned long access_bit;
@@ -201,8 +201,9 @@ landlock_get_deny_masks(const access_mask_t all_existing_optional_access,
if (WARN_ON_ONCE(!access_opt))
return 0;
- for (ssize_t i = ARRAY_SIZE(masks->access) - 1; i >= 0; i--) {
- const access_mask_t denied = masks->access[i] & optional_access;
+ for (ssize_t i = ARRAY_SIZE(masks->layers) - 1; i >= 0; i--) {
+ const access_mask_t denied = masks->layers[i].access &
+ optional_access;
const unsigned long newly_denied = denied & ~all_denied;
if (!newly_denied)
@@ -222,12 +223,12 @@ landlock_get_deny_masks(const access_mask_t all_existing_optional_access,
static void test_landlock_get_deny_masks(struct kunit *const test)
{
- const struct layer_access_masks layers1 = {
- .access[0] = LANDLOCK_ACCESS_FS_EXECUTE |
- LANDLOCK_ACCESS_FS_IOCTL_DEV,
- .access[1] = LANDLOCK_ACCESS_FS_TRUNCATE,
- .access[2] = LANDLOCK_ACCESS_FS_IOCTL_DEV,
- .access[9] = LANDLOCK_ACCESS_FS_EXECUTE,
+ const struct layer_masks layers1 = {
+ .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE |
+ LANDLOCK_ACCESS_FS_IOCTL_DEV,
+ .layers[1].access = LANDLOCK_ACCESS_FS_TRUNCATE,
+ .layers[2].access = LANDLOCK_ACCESS_FS_IOCTL_DEV,
+ .layers[9].access = LANDLOCK_ACCESS_FS_EXECUTE,
};
KUNIT_EXPECT_EQ(test, 0x1,
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index 35cac8f6daee..af100a8cd939 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -119,7 +119,7 @@ struct landlock_hierarchy {
deny_masks_t
landlock_get_deny_masks(const access_mask_t all_existing_optional_access,
const access_mask_t optional_access,
- const struct layer_access_masks *const masks);
+ const struct layer_masks *const masks);
int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy);
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index ff2c12e38bfc..c724692bb990 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -406,15 +406,15 @@ static const struct access_masks any_fs = {
* src_parent would result in having the same or fewer access rights if it were
* moved under new_parent.
*/
-static bool may_refer(const struct layer_access_masks *const src_parent,
- const struct layer_access_masks *const src_child,
- const struct layer_access_masks *const new_parent,
+static bool may_refer(const struct layer_masks *const src_parent,
+ const struct layer_masks *const src_child,
+ const struct layer_masks *const new_parent,
const bool child_is_dir)
{
- for (size_t i = 0; i < ARRAY_SIZE(new_parent->access); i++) {
- access_mask_t child_access = src_parent->access[i] &
- src_child->access[i];
- access_mask_t parent_access = new_parent->access[i];
+ for (size_t i = 0; i < ARRAY_SIZE(new_parent->layers); i++) {
+ access_mask_t child_access = src_parent->layers[i].access &
+ src_child->layers[i].access;
+ access_mask_t parent_access = new_parent->layers[i].access;
if (!child_is_dir) {
child_access &= ACCESS_FILE;
@@ -436,11 +436,11 @@ static bool may_refer(const struct layer_access_masks *const src_parent,
* that child2 may be used from parent2 to parent1 without increasing its access
* rights), false otherwise.
*/
-static bool no_more_access(const struct layer_access_masks *const parent1,
- const struct layer_access_masks *const child1,
+static bool no_more_access(const struct layer_masks *const parent1,
+ const struct layer_masks *const child1,
const bool child1_is_dir,
- const struct layer_access_masks *const parent2,
- const struct layer_access_masks *const child2,
+ const struct layer_masks *const parent2,
+ const struct layer_masks *const child2,
const bool child2_is_dir)
{
if (!may_refer(parent1, child1, parent2, child1_is_dir))
@@ -459,25 +459,25 @@ static bool no_more_access(const struct layer_access_masks *const parent1,
static void test_no_more_access(struct kunit *const test)
{
- const struct layer_access_masks rx0 = {
- .access[0] = LANDLOCK_ACCESS_FS_EXECUTE |
- LANDLOCK_ACCESS_FS_READ_FILE,
+ const struct layer_masks rx0 = {
+ .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE |
+ LANDLOCK_ACCESS_FS_READ_FILE,
};
- const struct layer_access_masks mx0 = {
- .access[0] = LANDLOCK_ACCESS_FS_EXECUTE |
- LANDLOCK_ACCESS_FS_MAKE_REG,
+ const struct layer_masks mx0 = {
+ .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE |
+ LANDLOCK_ACCESS_FS_MAKE_REG,
};
- const struct layer_access_masks x0 = {
- .access[0] = LANDLOCK_ACCESS_FS_EXECUTE,
+ const struct layer_masks x0 = {
+ .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE,
};
- const struct layer_access_masks x1 = {
- .access[1] = LANDLOCK_ACCESS_FS_EXECUTE,
+ const struct layer_masks x1 = {
+ .layers[1].access = LANDLOCK_ACCESS_FS_EXECUTE,
};
- const struct layer_access_masks x01 = {
- .access[0] = LANDLOCK_ACCESS_FS_EXECUTE,
- .access[1] = LANDLOCK_ACCESS_FS_EXECUTE,
+ const struct layer_masks x01 = {
+ .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE,
+ .layers[1].access = LANDLOCK_ACCESS_FS_EXECUTE,
};
- const struct layer_access_masks allows_all = {};
+ const struct layer_masks allows_all = {};
/* Checks without restriction. */
NMA_TRUE(&x0, &allows_all, false, &allows_all, NULL, false);
@@ -565,9 +565,13 @@ static void test_no_more_access(struct kunit *const test)
#undef NMA_TRUE
#undef NMA_FALSE
-static bool is_layer_masks_allowed(const struct layer_access_masks *masks)
+static bool is_layer_masks_allowed(const struct layer_masks *masks)
{
- return mem_is_zero(&masks->access, sizeof(masks->access));
+ for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) {
+ if (masks->layers[i].access)
+ return false;
+ }
+ return true;
}
/*
@@ -576,16 +580,16 @@ static bool is_layer_masks_allowed(const struct layer_access_masks *masks)
* Returns true if the request is allowed, false otherwise.
*/
static bool scope_to_request(const access_mask_t access_request,
- struct layer_access_masks *masks)
+ struct layer_masks *masks)
{
bool saw_unfulfilled_access = false;
if (WARN_ON_ONCE(!masks))
return true;
- for (size_t i = 0; i < ARRAY_SIZE(masks->access); i++) {
- masks->access[i] &= access_request;
- if (masks->access[i])
+ for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) {
+ masks->layers[i].access &= access_request;
+ if (masks->layers[i].access)
saw_unfulfilled_access = true;
}
return !saw_unfulfilled_access;
@@ -596,41 +600,46 @@ static bool scope_to_request(const access_mask_t access_request,
static void test_scope_to_request_with_exec_none(struct kunit *const test)
{
/* Allows everything. */
- struct layer_access_masks masks = {};
+ struct layer_masks masks = {};
/* Checks and scopes with execute. */
KUNIT_EXPECT_TRUE(test,
scope_to_request(LANDLOCK_ACCESS_FS_EXECUTE, &masks));
- KUNIT_EXPECT_EQ(test, 0, masks.access[0]);
+ KUNIT_EXPECT_EQ(test, 0, (access_mask_t)masks.layers[0].access);
}
static void test_scope_to_request_with_exec_some(struct kunit *const test)
{
/* Denies execute and write. */
- struct layer_access_masks masks = {
- .access[0] = LANDLOCK_ACCESS_FS_EXECUTE,
- .access[1] = LANDLOCK_ACCESS_FS_WRITE_FILE,
+ struct layer_masks masks = {
+ .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE,
+ .layers[1].access = LANDLOCK_ACCESS_FS_WRITE_FILE,
};
/* Checks and scopes with execute. */
KUNIT_EXPECT_FALSE(test, scope_to_request(LANDLOCK_ACCESS_FS_EXECUTE,
&masks));
- KUNIT_EXPECT_EQ(test, LANDLOCK_ACCESS_FS_EXECUTE, masks.access[0]);
- KUNIT_EXPECT_EQ(test, 0, masks.access[1]);
+ /*
+ * These casts to access_mask_t are needed because typeof(), used in
+ * KUNIT_EXPECT_EQ(), does not work on bitfields.
+ */
+ KUNIT_EXPECT_EQ(test, LANDLOCK_ACCESS_FS_EXECUTE,
+ (access_mask_t)masks.layers[0].access);
+ KUNIT_EXPECT_EQ(test, 0, (access_mask_t)masks.layers[1].access);
}
static void test_scope_to_request_without_access(struct kunit *const test)
{
/* Denies execute and write. */
- struct layer_access_masks masks = {
- .access[0] = LANDLOCK_ACCESS_FS_EXECUTE,
- .access[1] = LANDLOCK_ACCESS_FS_WRITE_FILE,
+ struct layer_masks masks = {
+ .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE,
+ .layers[1].access = LANDLOCK_ACCESS_FS_WRITE_FILE,
};
/* Checks and scopes without access request. */
KUNIT_EXPECT_TRUE(test, scope_to_request(0, &masks));
- KUNIT_EXPECT_EQ(test, 0, masks.access[0]);
- KUNIT_EXPECT_EQ(test, 0, masks.access[1]);
+ KUNIT_EXPECT_EQ(test, 0, (access_mask_t)masks.layers[0].access);
+ KUNIT_EXPECT_EQ(test, 0, (access_mask_t)masks.layers[1].access);
}
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
@@ -639,15 +648,15 @@ static void test_scope_to_request_without_access(struct kunit *const test)
* Returns true if there is at least one access right different than
* LANDLOCK_ACCESS_FS_REFER.
*/
-static bool is_eacces(const struct layer_access_masks *masks,
+static bool is_eacces(const struct layer_masks *masks,
const access_mask_t access_request)
{
if (!masks)
return false;
- for (size_t i = 0; i < ARRAY_SIZE(masks->access); i++) {
+ for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) {
/* LANDLOCK_ACCESS_FS_REFER alone must return -EXDEV. */
- if (masks->access[i] & access_request &
+ if (masks->layers[i].access & access_request &
~LANDLOCK_ACCESS_FS_REFER)
return true;
}
@@ -661,7 +670,7 @@ static bool is_eacces(const struct layer_access_masks *masks,
static void test_is_eacces_with_none(struct kunit *const test)
{
- const struct layer_access_masks masks = {};
+ const struct layer_masks masks = {};
IE_FALSE(&masks, 0);
IE_FALSE(&masks, LANDLOCK_ACCESS_FS_REFER);
@@ -671,8 +680,8 @@ static void test_is_eacces_with_none(struct kunit *const test)
static void test_is_eacces_with_refer(struct kunit *const test)
{
- const struct layer_access_masks masks = {
- .access[0] = LANDLOCK_ACCESS_FS_REFER,
+ const struct layer_masks masks = {
+ .layers[0].access = LANDLOCK_ACCESS_FS_REFER,
};
IE_FALSE(&masks, 0);
@@ -683,8 +692,8 @@ static void test_is_eacces_with_refer(struct kunit *const test)
static void test_is_eacces_with_write(struct kunit *const test)
{
- const struct layer_access_masks masks = {
- .access[0] = LANDLOCK_ACCESS_FS_WRITE_FILE,
+ const struct layer_masks masks = {
+ .layers[0].access = LANDLOCK_ACCESS_FS_WRITE_FILE,
};
IE_FALSE(&masks, 0);
@@ -743,11 +752,11 @@ static bool
is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
const struct path *const path,
const access_mask_t access_request_parent1,
- struct layer_access_masks *layer_masks_parent1,
+ struct layer_masks *layer_masks_parent1,
struct landlock_request *const log_request_parent1,
struct dentry *const dentry_child1,
const access_mask_t access_request_parent2,
- struct layer_access_masks *layer_masks_parent2,
+ struct layer_masks *layer_masks_parent2,
struct landlock_request *const log_request_parent2,
struct dentry *const dentry_child2)
{
@@ -755,9 +764,9 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
child1_is_directory = true, child2_is_directory = true;
struct path walker_path;
access_mask_t access_masked_parent1, access_masked_parent2;
- struct layer_access_masks _layer_masks_child1, _layer_masks_child2;
- struct layer_access_masks *layer_masks_child1 = NULL,
- *layer_masks_child2 = NULL;
+ struct layer_masks _layer_masks_child1, _layer_masks_child2;
+ struct layer_masks *layer_masks_child1 = NULL,
+ *layer_masks_child2 = NULL;
if (!access_request_parent1 && !access_request_parent2)
return true;
@@ -797,6 +806,10 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
}
if (unlikely(dentry_child1)) {
+ /*
+ * Get the layer masks for the child dentries for use by domain
+ * check later.
+ */
if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
&_layer_masks_child1,
LANDLOCK_KEY_INODE))
@@ -952,7 +965,7 @@ static int current_check_access_path(const struct path *const path,
};
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), masks, NULL);
- struct layer_access_masks layer_masks;
+ struct layer_masks layer_masks;
struct landlock_request request = {};
if (!subject)
@@ -1029,7 +1042,7 @@ static access_mask_t maybe_remove(const struct dentry *const dentry)
static bool collect_domain_accesses(const struct landlock_ruleset *const domain,
const struct dentry *const mnt_root,
struct dentry *dir,
- struct layer_access_masks *layer_masks_dom)
+ struct layer_masks *layer_masks_dom)
{
bool ret = false;
@@ -1135,8 +1148,7 @@ static int current_check_refer_path(struct dentry *const old_dentry,
access_mask_t access_request_parent1, access_request_parent2;
struct path mnt_dir;
struct dentry *old_parent;
- struct layer_access_masks layer_masks_parent1 = {},
- layer_masks_parent2 = {};
+ struct layer_masks layer_masks_parent1 = {}, layer_masks_parent2 = {};
struct landlock_request request1 = {}, request2 = {};
if (!subject)
@@ -1202,7 +1214,6 @@ static int current_check_refer_path(struct dentry *const old_dentry,
allow_parent2 = collect_domain_accesses(subject->domain, mnt_dir.dentry,
new_dir->dentry,
&layer_masks_parent2);
-
if (allow_parent1 && allow_parent2)
return 0;
@@ -1580,7 +1591,7 @@ static int hook_path_truncate(const struct path *const path)
*/
static void unmask_scoped_access(const struct landlock_ruleset *const client,
const struct landlock_ruleset *const server,
- struct layer_access_masks *const masks,
+ struct layer_masks *const masks,
const access_mask_t access)
{
int client_layer, server_layer;
@@ -1621,9 +1632,9 @@ static void unmask_scoped_access(const struct landlock_ruleset *const client,
server_walker = server_walker->parent;
for (; client_layer >= 0; client_layer--) {
- if (masks->access[client_layer] & access &&
+ if (masks->layers[client_layer].access & access &&
client_walker == server_walker)
- masks->access[client_layer] &= ~access;
+ masks->layers[client_layer].access &= ~access;
client_walker = client_walker->parent;
server_walker = server_walker->parent;
@@ -1635,7 +1646,7 @@ static int hook_unix_find(const struct path *const path, struct sock *other,
{
const struct landlock_ruleset *dom_other;
const struct landlock_cred_security *subject;
- struct layer_access_masks layer_masks;
+ struct layer_masks layer_masks;
struct landlock_request request = {};
static const struct access_masks fs_resolve_unix = {
.fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
@@ -1739,7 +1750,7 @@ static bool is_device(const struct file *const file)
static int hook_file_open(struct file *const file)
{
- struct layer_access_masks layer_masks = {};
+ struct layer_masks layer_masks = {};
access_mask_t open_access_request, full_access_request, allowed_access,
optional_access;
const struct landlock_cred_security *const subject =
@@ -1780,8 +1791,8 @@ static int hook_file_open(struct file *const file)
* are still unfulfilled in any of the layers.
*/
allowed_access = full_access_request;
- for (size_t i = 0; i < ARRAY_SIZE(layer_masks.access); i++)
- allowed_access &= ~layer_masks.access[i];
+ for (size_t i = 0; i < ARRAY_SIZE(layer_masks.layers); i++)
+ allowed_access &= ~layer_masks.layers[i].access;
}
/*
diff --git a/security/landlock/limits.h b/security/landlock/limits.h
index a4d908b240a2..08d5f2f6d321 100644
--- a/security/landlock/limits.h
+++ b/security/landlock/limits.h
@@ -31,6 +31,9 @@
#define LANDLOCK_MASK_SCOPE ((LANDLOCK_LAST_SCOPE << 1) - 1)
#define LANDLOCK_NUM_SCOPE __const_hweight64(LANDLOCK_MASK_SCOPE)
+#define LANDLOCK_NUM_ACCESS_MAX \
+ MAX(MAX(LANDLOCK_NUM_ACCESS_FS, LANDLOCK_NUM_ACCESS_NET), LANDLOCK_NUM_SCOPE)
+
#define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_TSYNC
#define LANDLOCK_MASK_RESTRICT_SELF ((LANDLOCK_LAST_RESTRICT_SELF << 1) - 1)
diff --git a/security/landlock/net.c b/security/landlock/net.c
index 0e697403eca9..d472e6cab12f 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -49,7 +49,7 @@ static int current_check_access_socket(struct socket *const sock,
{
unsigned short sock_family;
__be16 port;
- struct layer_access_masks layer_masks = {};
+ struct layer_masks layer_masks = {};
const struct landlock_rule *rule;
struct landlock_id id = {
.type = LANDLOCK_KEY_NET_PORT,
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 181df7736bb9..91948e406e69 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -628,7 +628,7 @@ landlock_find_rule(const struct landlock_ruleset *const ruleset,
* remaining unfulfilled access rights and masks has no leftover set bits).
*/
bool landlock_unmask_layers(const struct landlock_rule *const rule,
- struct layer_access_masks *masks)
+ struct layer_masks *masks)
{
if (!masks)
return true;
@@ -649,11 +649,17 @@ bool landlock_unmask_layers(const struct landlock_rule *const rule,
const struct landlock_layer *const layer = &rule->layers[i];
/* Clear the bits where the layer in the rule grants access. */
- masks->access[layer->level - 1] &= ~layer->access;
+ masks->layers[layer->level - 1].access &= ~layer->access;
+
+#ifdef CONFIG_AUDIT
+ /* Collect rule flags for each layer. */
+ if (layer->flags.quiet)
+ masks->layers[layer->level - 1].quiet = true;
+#endif /* CONFIG_AUDIT */
}
- for (size_t i = 0; i < ARRAY_SIZE(masks->access); i++) {
- if (masks->access[i])
+ for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) {
+ if (masks->layers[i].access)
return false;
}
return true;
@@ -668,6 +674,7 @@ get_access_mask_t(const struct landlock_ruleset *const ruleset,
*
* Populates @masks such that for each access right in @access_request,
* the bits for all the layers are set where this access right is handled.
+ * Rule flags are also zeroed.
*
* @domain: The domain that defines the current restrictions.
* @access_request: The requested access rights to check.
@@ -680,7 +687,7 @@ get_access_mask_t(const struct landlock_ruleset *const ruleset,
access_mask_t
landlock_init_layer_masks(const struct landlock_ruleset *const domain,
const access_mask_t access_request,
- struct layer_access_masks *const masks,
+ struct layer_masks *const masks,
const enum landlock_key_type key_type)
{
access_mask_t handled_accesses = 0;
@@ -709,11 +716,19 @@ landlock_init_layer_masks(const struct landlock_ruleset *const domain,
for (size_t i = 0; i < domain->num_layers; i++) {
const access_mask_t handled = get_access_mask(domain, i);
- masks->access[i] = access_request & handled;
- handled_accesses |= masks->access[i];
+ masks->layers[i].access = access_request & handled;
+ handled_accesses |= masks->layers[i].access;
+#ifdef CONFIG_AUDIT
+ masks->layers[i].quiet = false;
+#endif /* CONFIG_AUDIT */
+ }
+ for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->layers);
+ i++) {
+ masks->layers[i].access = 0;
+#ifdef CONFIG_AUDIT
+ masks->layers[i].quiet = false;
+#endif /* CONFIG_AUDIT */
}
- for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->access); i++)
- masks->access[i] = 0;
return handled_accesses;
}
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index 889f4b30301a..d0fa9af46a2c 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -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 logs for the object covered by this
+ * rule in this domain. For filesystem rules, this inherits down
+ * the file hierarchy.
+ */
+ u8 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).
@@ -302,12 +313,12 @@ landlock_get_scope_mask(const struct landlock_ruleset *const ruleset,
}
bool landlock_unmask_layers(const struct landlock_rule *const rule,
- struct layer_access_masks *masks);
+ struct layer_masks *masks);
access_mask_t
landlock_init_layer_masks(const struct landlock_ruleset *const domain,
const access_mask_t access_request,
- struct layer_access_masks *masks,
+ struct layer_masks *masks,
const enum landlock_key_type key_type);
#endif /* _SECURITY_LANDLOCK_RULESET_H */
--
2.54.0
^ permalink raw reply related
* [PATCH v11 0/9] Implement LANDLOCK_ADD_RULE_QUIET
From: Tingmao Wang @ 2026-06-12 1:48 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
Abhinav Saxena, linux-security-module
Hi,
This is the v11 of the "quiet flag" series, implementing the feature as
proposed in [1].
v10: https://lore.kernel.org/all/cover.1780272022.git.m@maowtm.org/
v9: https://lore.kernel.org/all/cover.1779843375.git.m@maowtm.org/
v8: https://lore.kernel.org/all/cover.1775490344.git.m@maowtm.org/
v7: https://lore.kernel.org/all/cover.1766330134.git.m@maowtm.org/
v6: https://lore.kernel.org/all/cover.1765040503.git.m@maowtm.org/
v5: https://lore.kernel.org/all/cover.1763931318.git.m@maowtm.org/
v4: https://lore.kernel.org/all/cover.1763330228.git.m@maowtm.org/
v3: https://lore.kernel.org/all/cover.1761511023.git.m@maowtm.org/
v2: https://lore.kernel.org/all/cover.1759686613.git.m@maowtm.org/
v1: https://lore.kernel.org/all/cover.1757376311.git.m@maowtm.org/
v10..v11:
- doc and style fixes
- s/audit log/log/
- u32 flags arguments
- stop using bitfields for quiet_optional_accesses and fown_layer
- fixed bitfield using different types
- use u8 instead of bool in struct landlock_layer
- sandboxer: merge LL_{FS,NET,SCOPED}_QUIET_ACCESS into a single
LL_QUIET_ACCESS with more descriptive values.
- selftests: also test the quiet_access_net and quiet_scoped fields.
(Kept ABI version at 10)
All text following this line is unchanged except for the demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
v9..v10:
- clang-format on .h files
- doc changes
- remove stray __attribute__((fallthrough));
v8..v9:
- Refactor to store the collected rule flags in layer_masks instead
(renamed from layer_access_masks). Got rid of layer_mask_t again.
- Rebase sandboxer and net_tests on top of UDP support, resolving
conflicts
- Additional small changes, noted in each patch
v7..v8:
- Rebase to mic/next
- Re-introduced layer_mask_t due to need in first patch
- Plumb through rule flags in hook_unix_find()
- Some selftests patches were not properly clang-format'd, fixed now.
- Minor env var handling change in sandboxer
- Fix selftests use of audit_count_records() without EXPECT_EQ
v6..v7:
- Remove "landlock: Fix wrong type usage" (merged)
- Revert back to taking rule_flags separately from landlock_request until
we call landlock_log_denial (https://lore.kernel.org/all/20251219.ahn3aiJuKahb@digikod.net/)
- Rebase to mic/next
v5..v6 rebases on top of the new simpler disconnected directory handling,
change some bools into u32, and fix some typo and style.
v4..v5 addresses review feedbacks, most significantly:
- reduces code changes by pushing rule_flags into landlock_request.
- adding test cases for two layers handling different access bits.
v3..v4 is a one-character formatting change, plus more tests.
We now have 5 patches for the selftest - I'm happy to squash it into one
depending on preference (and happy for Mickaël to do the squash if no
other feedback):
- 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
- selftests/landlock: Add tests for quiet flag with scope
- selftests/landlock: Add tests for invalid use of quiet flag
v2..v3:
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).
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.
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_QUIET_ACCESS=read ./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 (9):
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
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
selftests/landlock: Add tests for quiet flag with scope
selftests/landlock: Add tests for invalid use of quiet flag
Documentation/admin-guide/LSM/landlock.rst | 9 +-
Documentation/userspace-api/landlock.rst | 14 +
include/uapi/linux/landlock.h | 61 +
samples/landlock/sandboxer.c | 138 +-
security/landlock/access.h | 44 +-
security/landlock/audit.c | 288 +-
security/landlock/audit.h | 3 +-
security/landlock/domain.c | 57 +-
security/landlock/domain.h | 11 +-
security/landlock/fs.c | 157 +-
security/landlock/fs.h | 21 +-
security/landlock/limits.h | 3 +
security/landlock/net.c | 22 +-
security/landlock/net.h | 5 +-
security/landlock/ruleset.c | 45 +-
security/landlock/ruleset.h | 29 +-
security/landlock/syscalls.c | 71 +-
tools/testing/selftests/landlock/audit_test.c | 27 +-
tools/testing/selftests/landlock/base_test.c | 118 +-
tools/testing/selftests/landlock/common.h | 2 +
tools/testing/selftests/landlock/fs_test.c | 2450 ++++++++++++++++-
tools/testing/selftests/landlock/net_test.c | 138 +-
.../landlock/scoped_abstract_unix_test.c | 77 +-
23 files changed, 3576 insertions(+), 214 deletions(-)
base-commit: a6f0a6f5377fae42a8028f63c89d544c68f24b60
--
2.54.0
^ permalink raw reply
* Re: [PATCH v10 4/9] samples/landlock: Add quiet flag support to sandboxer
From: Tingmao Wang @ 2026-06-12 1:12 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Günther Noack, Justin Suess, Jan Kara, Abhinav Saxena,
linux-security-module
In-Reply-To: <20260609.Pi8aiyae5nee@digikod.net>
On 6/8/26 23:41, Mickaël Salaün wrote:
> As for LL_FORCE_LOG, using a QUIET flag not supported should exit with
> an error.
As in, if the current kernel doesn't support quiet flags? Added check.
>
> On Mon, Jun 01, 2026 at 01:00:38AM +0100, Tingmao Wang wrote:
>> Adds 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.
>>
>> Signed-off-by: Tingmao Wang <m@maowtm.org>
>> ---
>>
>> Changes in v10:
>> - Remove stray __attribute__((fallthrough)); (Thanks Justin for
>> spotting)
>>
>> Changes in v9:
>> - Add udp connect / bind quiet flag support
>>
>> Changes in v8:
>> - Rebase on top of mic/next
>> - populate_ruleset_net() already does not require the env var to be
>> present, so remove redundant comment and check above
>> populate_ruleset_net(ENV_NET_QUIET_NAME, ...).
>>
>> Changes in v6:
>> - Make populate_ruleset_{fs,net} take a flags argument instead of a bool
>> quiet (suggested by Justin Suess)
>> - Fix if braces style
>>
>> Changes in v3:
>> - Minor change to the above commit message.
>>
>> Changes in v2:
>> - 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, 122 insertions(+), 11 deletions(-)
>>
>> diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
>> index 94e399e6b146..73a81ecd3696 100644
>> --- a/samples/landlock/sandboxer.c
>> +++ b/samples/landlock/sandboxer.c
>> @@ -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_UDP_BIND_NAME "LL_UDP_BIND"
>> #define ENV_UDP_CONNECT_SEND_NAME "LL_UDP_CONNECT_SEND"
>> @@ -119,7 +124,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, __u32 flags)
>> {
>> int num_paths, i, ret = 1;
>> char *env_path_name;
>> @@ -169,7 +174,7 @@ 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, flags)) {
>> fprintf(stderr,
>> "Failed to update the ruleset with \"%s\": %s\n",
>> path_list[i], strerror(errno));
>> @@ -187,7 +192,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, __u32 flags)
>> {
>> int ret = 1;
>> char *env_port_name, *env_port_name_next, *strport;
>> @@ -215,7 +220,7 @@ 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, flags)) {
>> fprintf(stderr,
>> "Failed to update the ruleset with port \"%llu\": %s\n",
>> net_port.port, strerror(errno));
>> @@ -303,6 +308,58 @@ static bool check_ruleset_scope(const char *const env_var,
>>
>> /* clang-format on */
>>
>> +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;
>
> What happen if we set "b" in LL_FS_QUIET_ACCESS?
>
>> + else if (strcmp(str_access, "c") == 0)
>> + *quiet_access |= LANDLOCK_ACCESS_NET_CONNECT_TCP;
>> + else if (strcmp(str_access, "ub") == 0)
>
> I don't really like these access-right names, they are not consistent.
> All these env variables add a lot of complexity too. What about just
> being able to quiet a path or a port? That would mean renaming
> LL_FS_QUIET_ACCESS to LL_FS_QUIET.
I'm happy to remove LL_{FS,NET}_QUIET_ACCESS and just have
LL_{FS,NET}_QUIET quiet all access, but then we lose the ability to demo
"quiet only read but still log write" via the sandboxer.
I do agree the "b in LL_FS_QUIET_ACCESS" case is weird, so maybe we can
just have one LL_QUIET_ACCESS variable?
Also, the names are like this because I tried to mimic the one-letter
scoped access, but we could use e.g.
LL_QUIET_ACCESS=read:write:tcp_bind:tcp_connect:udp_bind:udp_connect:abstract_unix_socket:signal
Do you want to keep the ability to specify LL_QUIET_ACCESS? (I think it's
useful for demo, since I expect "quiet read but log write denials" to be
quite common.)
>
> Anyway, all should be unsetenv() unconditionally.
I think they are already all unsetenv()'d already (checked with
/usr/bin/env), do you mean to make them not conditional on the env
existing in the first place? I followed how populate_ruleset_{fs,net}
works and those two functions currently do conditional unsetenv(),
althouygh check_ruleset_scope() does it unconditionally.
>
>> + *quiet_access |= LANDLOCK_ACCESS_NET_BIND_UDP;
>> + else if (strcmp(str_access, "uc") == 0)
>> + *quiet_access |= LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP;
>> + 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 10
>>
>> #define XSTR(s) #s
>> [...]
^ permalink raw reply
* Re: [PATCH v10 3/9] landlock: Suppress logging when quiet flag is present
From: Tingmao Wang @ 2026-06-12 1:11 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Günther Noack, Justin Suess, Jan Kara, Abhinav Saxena,
linux-security-module
In-Reply-To: <20260608.daeshu7Leequ@digikod.net>
On 6/8/26 23:41, Mickaël Salaün wrote:
> On Mon, Jun 01, 2026 at 01:00:37AM +0100, Tingmao Wang wrote:
>> [...]
>> @@ -265,20 +268,33 @@ get_layer_from_deny_masks(access_mask_t *const access_request,
>> BITS_PER_TYPE(access_mask_t)) {
>> if (access_req & BIT(access_bit)) {
>> const size_t layer =
>> - (deny_masks >> (access_index * 4)) &
>> + (deny_masks >>
>> + (access_index *
>> + HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1))) &
>> (LANDLOCK_MAX_NUM_LAYERS - 1);
>> + const bool layer_has_quiet =
>> + !!(quiet_optional_accesses & BIT(access_index));
>>
>> if (layer > youngest_layer) {
>> youngest_layer = layer;
>> missing = BIT(access_bit);
>> + should_quiet = layer_has_quiet;
>> } else if (layer == youngest_layer) {
>> missing |= BIT(access_bit);
>> + /*
>> + * Whether the layer has rules with quiet flag covering
>> + * the file accessed does not depend on the access, and so
>> + * the following WARN_ON_ONCE() should not fail.
>> + */
>> + WARN_ON_ONCE(should_quiet && !layer_has_quiet);
>
> WARN_ON_ONCE(should_quiet != layer_has_quiet);
That will fail when layer 0 has quiet flag on the object, at which point
since youngest_layer starts from 0, we will reach this branch with
should_quiet initialized earlier to false, but layer_has_quiet == true.
Also, if should_quiet != layer_has_quiet is always false here, then the
next line is not necessary.
>
>> + should_quiet = layer_has_quiet;
Would it be clearer to do should_quiet |= layer_has_quiet, mimicking the
"missing |= BIT(access_bit)"? (The result is the same)
>> }
>> }
>> access_index++;
>> }
>>
>> *access_request = missing;
>> + *quiet = should_quiet;
>> return youngest_layer;
>> }
>>
>> [...]
>> diff --git a/security/landlock/fs.h b/security/landlock/fs.h
>> index cb7e654933ac..d0fca7da2466 100644
>> --- a/security/landlock/fs.h
>> +++ b/security/landlock/fs.h
>> @@ -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;
>
> Please don't hardcode such size.
>
> Anyway, fown_layer can be updated concurrently (holding a lock), so we
> should not convert it to a bitfield.
>
>> #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 */
>
> Remaining useless ifdef/endif.
Since we are not using bitfield for fown_layer anymore, I've also turned
quiet_optional_accesses (u8) into a non-bitfield. Then I reverted this
deletion and added
static_assert((typeof_member(struct landlock_file_security,
quiet_optional_accesses)) ~0 >=
HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL));
>
>> [...]
^ permalink raw reply
* Re: [PATCH v10 1/9] landlock: Add a place for flags to layer rules
From: Tingmao Wang @ 2026-06-12 1:11 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Günther Noack, Justin Suess, Jan Kara, Abhinav Saxena,
linux-security-module
In-Reply-To: <20260608.ni6daelae9Qu@digikod.net>
On 6/8/26 23:40, Mickaël Salaün wrote:
> On Mon, Jun 01, 2026 at 01:00:35AM +0100, Tingmao Wang wrote:
>> [...]
>> diff --git a/security/landlock/access.h b/security/landlock/access.h
>> index c19d5bc13944..42d8b5134358 100644
>> --- a/security/landlock/access.h
>> +++ b/security/landlock/access.h
>> @@ -62,18 +62,39 @@ static_assert(sizeof(typeof_member(union access_masks_all, masks)) ==
>> sizeof(typeof_member(union access_masks_all, all)));
>>
>> /**
>> - * struct layer_access_masks - A boolean matrix of layers and access rights
>> - *
>> - * This has a bit for each combination of layer numbers and access rights.
>> - * During access checks, it is used to represent the access rights for each
>> - * layer which still need to be fulfilled. When all bits are 0, the access
>> - * request is considered to be fulfilled.
>> + * struct layer_mask - The unfulfilled access rights and rule flags for
>
> This struct could be used to store "fulfilled" access rights too. The
> previous description is more accurate. Please keep most of the previous
> description too and adjust as needed.
In v10 I attempted to move this description to the doc strings for the
fields, but happy to move back (done in v11).
^ permalink raw reply
* Re: [PATCH] Add LoadPin support for eBPF program loading
From: David Windsor @ 2026-06-12 0:08 UTC (permalink / raw)
To: alex.roberts109, Kees Cook, Paul Moore, James Morris,
Serge E . Hallyn
Cc: linux-kernel, linux-security-module, bpf, Alexei Starovoitov,
KP Singh, David Windsor
In-Reply-To: <20260611-b4-rfc-loadpin-ebpf-v1-1-11a6c8e6170d@outlook.com>
On Thu, Jun 11, 2026 at 01:59:10PM -0500, Alex Roberts wrote:
> +static int loadpin_bpf_prog_load(struct bpf_prog *prog, union bpf_attr *attr,
> + struct bpf_token *token, bool is_kernel)
> +{
> + int res = 0;
> + struct file *exe_file = NULL;
> + struct mm_struct *mm = current->mm;
> +
> + if (is_kernel || !mm)
> + return 0;
> +
> + exe_file = get_mm_exe_file(mm);
> + if (!exe_file)
> + return 0;
> +
> + res = loadpin_check(exe_file, READING_EBPF);
Why are we checking current here? IIUC this will be whoever calls
bpf(2), which would be the loader, which would then be able to load bpf
programs from an untrusted source.
In the kmod case loadpin_check() sees the .ko itself.
^ permalink raw reply
* Re: [PATCH bpf-next 0/5] Verify BPF signed loader at load time
From: Paul Moore @ 2026-06-11 22:56 UTC (permalink / raw)
To: Daniel Borkmann
Cc: ast, kpsingh, James.Bottomley, bboscaccy, memxor, torvalds, bpf,
linux-security-module
In-Reply-To: <20260610230329.727075-1-daniel@iogearbox.net>
On Wed, Jun 10, 2026 at 7:03 PM Daniel Borkmann <daniel@iogearbox.net> wrote:
>
> The BPF signing scheme signs a light skeleton's loader program and lets
> the loader vouch for everything else: bpftool bakes the SHA256 of the
> metadata map into the loader's instructions, signs the instructions, and
> the loader compares the (frozen, exclusive) map against that hash from
> within BPF once it runs. The construction is sound as a trusted hash
> chain, but the kernel itself never attests the metadata, and that split
> has been the recurring objection from the LSM / integrity side since the
> scheme was proposed.
>
> This proposal closes both gaps by having the kernel verify the metadata
> at BPF_PROG_LOAD time, before the LSM admission hook and before the
> verifier, /without/ growing the UAPI. A signed loader binds its metadata
> map(s) through the existing fd_array/fd_array_cnt, and exclusive maps
> are already bound to the loader's digest via excl_prog_hash. When a
> signature is present, the kernel collects the exclusive maps from the
> fd_array and appends their frozen contents to the instructions before
> PKCS#7 verification, so the signature covers ...
>
> insns || metadata_0 || metadata_1 || [...]
>
> ... in fd_array order. The in-loader hash check is dropped from the
> gen_loader entirely: generated loaders carry no verification logic
> anymore, and signing or verifying a skeleton becomes an ordinary CMS
> operation over bytes that sit verbatim in the skeleton, reproducible
> offline. A signed program is either BPF_SIG_UNSIGNED or BPF_SIG_VERIFIED
> with nothing in between.
I'll be honest and say I'm a bit surprised to see this patchset,
especially since KP and Alexei argued so strongly against this
signature scheme, preferring KP's scheme where the loader verified the
maps. I'd be curious to hear the reason for the change of heart if
you can share it. Regardless of the motivation for this change, I
obviously think this is a significant improvement over KP's signature
scheme which shipped in Linux v6.18.
I also think it is worth mentioning the similarities to work Blaise
did before the most recent Hornet version:
https://lore.kernel.org/linux-security-module/20250929213520.1821223-1-bboscaccy@linux.microsoft.com/
While Blaise's patchset added to the UAPI, that was done simply to
retain compatibility with KP's signature scheme; your patchset does
without any UAPI additions, but loses compatibility with existing
signed lskels. Beyond that, the basic signature scheme between
Blaise's patchset and what you are proposing appears the same ...
which is a good thing as far as I'm concerned.
--
paul-moore.com
^ permalink raw reply
* [PATCH] Add LoadPin support for eBPF program loading
From: Alex Roberts via B4 Relay @ 2026-06-11 18:59 UTC (permalink / raw)
To: Kees Cook, Paul Moore, James Morris, Serge E. Hallyn
Cc: linux-kernel, linux-security-module, bpf, Alexei Starovoitov,
KP Singh, Alex Roberts
From: Alex Roberts <alex.roberts109@outlook.com>
Add LoadPin LSM hook to bpf_prog_load to prevent loading of BPF
programs from untrusted filesystems
---
There have been several efforts to provide a trust mechanism for eBPF programs –
particularly in the form of a signed program. After many discussions, BPF
Signing became supported [1].
This patch series intends to provide an alternative trust mechanism for eBPF
programs using LoadPin and the LoadPin dm-verity support. The approach is to pin
eBPF userspace loaders to a single filesystem or to one or more dm-verity
integrity protected filesystems. This is similar to the existing Loadpin+verity
implementation and approach of e.g., loading unsigned kernel modules or kexec
images from a pinned trusted filesystem [2].
When a userspace application attempts to load a BPF program, LoadPin
first checks whether the exe_file is located on the pinned root, if so
bpf_prog_load is allowed. Otherwise, Loadpin denies bpf_prog_load. Additionally,
if verity support is enabled, LoadPin determines whether the exe_file is located
on a verity backed device and whether the root digest of that device is in the
list of trusted digests. bpf_prog_load is allowed if the verity device has a
trusted root digest.
Background:
In a secure boot environment, secure boot can be extended to the root filesystem
by means of dm-verity. Placing userspace eBPF programs on trusted filesystems
and restricting their use to a trusted filesystem implicitly extends the trust
to the underlying BPF program itself, without having to sign the bytecode.
Rather than sign each program and load keys into the kernel keyring, it can be
sufficient to sign the filesystem.
Additional Considerations:
Because the userspace loader itself defines the filesystem to pin from, it
should be acknowledged that this does not necessarily solve the dynamically
generated eBPF usecase. For example, if the pinned filesystem includes bpftrace
or other userspace applications that dynamically generate and load eBPF
bytecode, this trust mechanism does not apply. But for systems that either
1) remove/do-not-provide dynamically generating programs or
2) loadpin filesystem(s) that exclude such programs,
then it can be reasoned that only trusted bytecode is loaded.
[1] https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git/commit/?h=for-next&id=58a5820582e4c809dd26b3f2d396cf072411d6e8
[2] https://lore.kernel.org/lkml/20220517163437.v4.2.I01c67af41d2f6525c6d023101671d7339a9bc8b5@changeid/
Signed-off-by: Alex Roberts <alex.roberts109@outlook.com>
---
include/linux/kernel_read_file.h | 1 +
security/loadpin/Kconfig | 12 ++++++++++++
security/loadpin/loadpin.c | 29 +++++++++++++++++++++++++++++
3 files changed, 42 insertions(+)
diff --git a/include/linux/kernel_read_file.h b/include/linux/kernel_read_file.h
index d613a7b4dd35..75cbd514562f 100644
--- a/include/linux/kernel_read_file.h
+++ b/include/linux/kernel_read_file.h
@@ -15,6 +15,7 @@
id(POLICY, security-policy) \
id(X509_CERTIFICATE, x509-certificate) \
id(MODULE_COMPRESSED, kernel-module-compressed) \
+ id(EBPF, ebpf) \
id(MAX_ID, )
#define __fid_enumify(ENUM, dummy) READING_ ## ENUM,
diff --git a/security/loadpin/Kconfig b/security/loadpin/Kconfig
index aef63d3e30df..be0754735d5d 100644
--- a/security/loadpin/Kconfig
+++ b/security/loadpin/Kconfig
@@ -42,3 +42,15 @@ config SECURITY_LOADPIN_VERITY
This is followed by the verity digests, with one digest per
line.
+
+config SECURITY_LOADPIN_EBPF
+ bool "Pin ebpf programs to one filesystem"
+ depends on SECURITY_LOADPIN
+ help
+ eBPF program loading is pinned to the first filesystem from which the
+ application loading the progam resides. When enabled, applications on
+ other filesystems that attempt to load an eBPF program will be rejected.
+ This is best used when applications that load eBPF programs reside on a
+ read-only filesystem from which tools for dynamically generating eBPF
+ programs such as bpftrace are not installed. This will not restrict the
+ kernel from loading a bpf program.
\ No newline at end of file
diff --git a/security/loadpin/loadpin.c b/security/loadpin/loadpin.c
index f71861f98e1a..47fd7e247edb 100644
--- a/security/loadpin/loadpin.c
+++ b/security/loadpin/loadpin.c
@@ -23,6 +23,10 @@
#include <uapi/linux/loadpin.h>
#include <uapi/linux/lsm.h>
+#ifdef CONFIG_SECURITY_LOADPIN_EBPF
+#include <linux/bpf.h>
+#endif /* CONFIG_SECURITY_LOADPIN_BPF */
+
#define VERITY_DIGEST_FILE_HEADER "# LOADPIN_TRUSTED_VERITY_ROOT_DIGESTS"
static void report_load(const char *origin, struct file *file, char *operation)
@@ -204,6 +208,28 @@ static int loadpin_load_data(enum kernel_load_data_id id, bool contents)
return loadpin_check(NULL, (enum kernel_read_file_id) id);
}
+#ifdef CONFIG_SECURITY_LOADPIN_EBPF
+static int loadpin_bpf_prog_load(struct bpf_prog *prog, union bpf_attr *attr,
+ struct bpf_token *token, bool is_kernel)
+{
+ int res = 0;
+ struct file *exe_file = NULL;
+ struct mm_struct *mm = current->mm;
+
+ if (is_kernel || !mm)
+ return 0;
+
+ exe_file = get_mm_exe_file(mm);
+ if (!exe_file)
+ return 0;
+
+ res = loadpin_check(exe_file, READING_EBPF);
+ fput(exe_file);
+
+ return res;
+}
+#endif /* CONFIG_SECURITY_LOADPIN_EBPF */
+
static const struct lsm_id loadpin_lsmid = {
.name = "loadpin",
.id = LSM_ID_LOADPIN,
@@ -213,6 +239,9 @@ static struct security_hook_list loadpin_hooks[] __ro_after_init = {
LSM_HOOK_INIT(sb_free_security, loadpin_sb_free_security),
LSM_HOOK_INIT(kernel_read_file, loadpin_read_file),
LSM_HOOK_INIT(kernel_load_data, loadpin_load_data),
+#ifdef CONFIG_SECURITY_LOADPIN_EBPF
+ LSM_HOOK_INIT(bpf_prog_load, loadpin_bpf_prog_load),
+#endif /* CONFIG_SECURITY_LOADPIN_EBPF */
};
static void __init parse_exclude(void)
---
base-commit: 122b52f0bab007ebeb414c8280c1def17b9ed1f4
change-id: 20260611-b4-rfc-loadpin-ebpf-086c41deb503
Best regards,
--
Alex Roberts <alex.roberts109@outlook.com>
^ permalink raw reply related
* [PATCH v5 6/6] landlock: Add documentation for UDP support
From: Matthieu Buffet @ 2026-06-11 16:21 UTC (permalink / raw)
To: Mickaël Salaün, Günther Noack
Cc: linux-security-module, Mikhail Ivanov, konstantin.meskhidze,
Tingmao Wang, netdev, Matthieu Buffet
In-Reply-To: <20260611162107.49278-1-matthieu@buffet.re>
Add example of UDP usage, without detailing the two access right.
Slightly change the example used in code blocks: build a ruleset for a
DNS client, so that it uses both TCP and UDP.
Signed-off-by: Matthieu Buffet <matthieu@buffet.re>
---
Documentation/userspace-api/landlock.rst | 91 +++++++++++++++++++-----
1 file changed, 72 insertions(+), 19 deletions(-)
diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
index 45861fa75685..0ea55c2c732c 100644
--- a/Documentation/userspace-api/landlock.rst
+++ b/Documentation/userspace-api/landlock.rst
@@ -8,7 +8,7 @@ Landlock: unprivileged access control
=====================================
:Author: Mickaël Salaün
-:Date: May 2026
+:Date: June 2026
The goal of Landlock is to enable restriction of ambient rights (e.g. global
filesystem or network access) for a set of processes. Because Landlock
@@ -40,8 +40,8 @@ Filesystem rules
and the related filesystem actions are defined with
`filesystem access rights`.
-Network rules (since ABI v4)
- For these rules, the object is a TCP port,
+Network rules (since ABI v4 for TCP and v10 for UDP)
+ For these rules, the object is a TCP or UDP port,
and the related actions are defined with `network access rights`.
Defining and enforcing a security policy
@@ -49,11 +49,11 @@ Defining and enforcing a security policy
We first need to define the ruleset that will contain our rules.
-For this example, the ruleset will contain rules that only allow filesystem
-read actions and establish a specific TCP connection. Filesystem write
-actions and other TCP actions will be denied.
+For this example, the ruleset will contain rules that only allow some
+filesystem read actions and some specific UDP and TCP actions. Filesystem
+write actions and other TCP/UDP actions will be denied.
-The ruleset then needs to handle both these kinds of actions. This is
+The ruleset then needs to handle all these kinds of actions. This is
required for backward and forward compatibility (i.e. the kernel and user
space may not know each other's supported restrictions), hence the need
to be explicit about the denied-by-default access rights.
@@ -81,7 +81,9 @@ to be explicit about the denied-by-default access rights.
LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
.handled_access_net =
LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ LANDLOCK_ACCESS_NET_CONNECT_TCP |
+ LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
.scoped =
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
LANDLOCK_SCOPE_SIGNAL,
@@ -132,6 +134,12 @@ version, and only use the available subset of access rights:
case 6 ... 8:
/* Removes LANDLOCK_ACCESS_FS_RESOLVE_UNIX for ABI < 9 */
ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_RESOLVE_UNIX;
+ __attribute__((fallthrough));
+ case 9:
+ /* Removes LANDLOCK_ACCESS_NET_*_UDP for ABI < 10 */
+ ruleset_attr.handled_access_net &=
+ ~(LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
}
This enables the creation of an inclusive ruleset that will contain our rules.
@@ -187,20 +195,52 @@ kernel does not support are dropped (the compatibility switch above already
cleared them in ``handled_access_*``), and the rule is skipped if no supported
right remains.
-For network access-control, we can add a set of rules that allow to use a port
-number for a specific action: HTTPS connections.
+For network access-control, we will add a set of rules to allow DNS
+queries, which requires both UDP and TCP. For TCP, we need to allow
+outbound connections to port 53, which can be handled and granted starting
+with ABI 4:
.. code-block:: c
- struct landlock_net_port_attr net_port = {
+ struct landlock_net_port_attr tcp_conn = {
.allowed_access = LANDLOCK_ACCESS_NET_CONNECT_TCP,
- .port = 443,
+ .port = 53,
+ };
+
+ tcp_conn.allowed_access &= ruleset_attr.handled_access_net;
+ if (tcp_conn.allowed_access)
+ err = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+ &tcp_conn, 0);
+
+We also need to be able to send UDP datagrams to port 53, which requires
+granting ``LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP``. Since our DNS client will
+emit datagrams without explicitly binding to a specific source port, its UDP
+socket will automatically bind an ephemeral port. To allow this behaviour,
+we also need to grant ``LANDLOCK_ACCESS_NET_BIND_UDP`` on port 0, as if
+the program explicitly called :manpage:`bind(2)` on port 0.
+
+.. code-block:: c
+
+ struct landlock_net_port_attr udp_send = {
+ .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
+ .port = 53,
+ };
+
+ udp_send.allowed_access &= ruleset_attr.handled_access_net;
+ if (udp_send.allowed_access)
+ err = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+ &udp_send, 0);
+ [...]
+
+ struct landlock_net_port_attr udp_bind = {
+ .allowed_access = LANDLOCK_ACCESS_NET_BIND_UDP,
+ .port = 0,
};
- net_port.allowed_access &= ruleset_attr.handled_access_net;
- if (net_port.allowed_access)
+ udp_bind.allowed_access &= ruleset_attr.handled_access_net;
+ if (udp_bind.allowed_access)
err = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &net_port, 0);
+ &udp_bind, 0);
When passing a non-zero ``flags`` argument to ``landlock_restrict_self()``, a
similar backwards compatibility check is needed for the restrict flags
@@ -234,7 +274,7 @@ similar backwards compatibility check is needed for the restrict flags
The next step is to restrict the current thread from gaining more privileges
(e.g. through a SUID binary). We now have a ruleset with the first rule
allowing read and execute access to ``/usr`` while denying all other handled
-accesses for the filesystem, and a second rule allowing HTTPS connections.
+accesses for the filesystem, and two more rules allowing DNS queries.
.. code-block:: c
@@ -722,6 +762,19 @@ Starting with the Landlock ABI version 9, it is possible to restrict
connections to pathname UNIX domain sockets (:manpage:`unix(7)`) using
the new ``LANDLOCK_ACCESS_FS_RESOLVE_UNIX`` right.
+UDP bind, connect and send* (ABI < 10)
+--------------------------------------
+
+Starting with the Landlock ABI version 10, it is possible to restrict
+setting the local port of UDP sockets with the
+``LANDLOCK_ACCESS_NET_BIND_UDP`` right. This includes restricting the
+ability to trigger autobind of an ephemeral port by the kernel by e.g.
+sending a first datagram or setting the remote peer of a socket.
+The ``LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP`` right controls setting the
+remote port of UDP sockets (via :manpage:`connect(2)), and sending
+datagrams to an explicit remote port (ignoring any destination set on
+UDP sockets, via e.g. :manpage:`sendto(2)).
+
.. _kernel_support:
Kernel support
@@ -784,10 +837,10 @@ the boot loader.
Network support
---------------
-To be able to explicitly allow TCP operations (e.g., adding a network rule with
-``LANDLOCK_ACCESS_NET_BIND_TCP``), the kernel must support TCP
+To be able to explicitly allow TCP or UDP operations (e.g., adding a network rule with
+``LANDLOCK_ACCESS_NET_BIND_TCP``), the kernel must support the TCP/IP protocol suite
(``CONFIG_INET=y``). Otherwise, sys_landlock_add_rule() returns an
-``EAFNOSUPPORT`` error, which can safely be ignored because this kind of TCP
+``EAFNOSUPPORT`` error, which can safely be ignored because this kind of TCP or UDP
operation is already not possible.
Questions and answers
--
2.47.3
^ permalink raw reply related
* [PATCH v5 5/6] samples/landlock: Add sandboxer UDP access control
From: Matthieu Buffet @ 2026-06-11 16:21 UTC (permalink / raw)
To: Mickaël Salaün, Günther Noack
Cc: linux-security-module, Mikhail Ivanov, konstantin.meskhidze,
Tingmao Wang, netdev, Matthieu Buffet
In-Reply-To: <20260611162107.49278-1-matthieu@buffet.re>
Add environment variables to control associated access rights:
- LL_UDP_BIND
- LL_UDP_CONNECT_SEND
Each one takes a list of ports separated by colons, like other list
options.
Signed-off-by: Matthieu Buffet <matthieu@buffet.re>
---
samples/landlock/sandboxer.c | 41 ++++++++++++++++++++++++++++++++++--
1 file changed, 39 insertions(+), 2 deletions(-)
diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
index 66e56ae275c6..f44db2857bbf 100644
--- a/samples/landlock/sandboxer.c
+++ b/samples/landlock/sandboxer.c
@@ -62,6 +62,8 @@ static inline int landlock_restrict_self(const int ruleset_fd,
#define ENV_TCP_CONNECT_NAME "LL_TCP_CONNECT"
#define ENV_SCOPED_NAME "LL_SCOPED"
#define ENV_FORCE_LOG_NAME "LL_FORCE_LOG"
+#define ENV_UDP_BIND_NAME "LL_UDP_BIND"
+#define ENV_UDP_CONNECT_SEND_NAME "LL_UDP_CONNECT_SEND"
#define ENV_DELIMITER ":"
static int str2num(const char *numstr, __u64 *num_dst)
@@ -301,7 +303,7 @@ static bool check_ruleset_scope(const char *const env_var,
/* clang-format on */
-#define LANDLOCK_ABI_LAST 9
+#define LANDLOCK_ABI_LAST 10
#define XSTR(s) #s
#define STR(s) XSTR(s)
@@ -324,6 +326,11 @@ static const char help[] =
"means an empty list):\n"
"* " ENV_TCP_BIND_NAME ": ports allowed to bind (server)\n"
"* " ENV_TCP_CONNECT_NAME ": ports allowed to connect (client)\n"
+ "* " ENV_UDP_BIND_NAME ": local UDP ports allowed to bind (server: "
+ "prepare to receive on port / client: set as source port)\n"
+ "* " ENV_UDP_CONNECT_SEND_NAME ": remote UDP ports allowed to connect "
+ "or send to (client: use as destination port / server: receive only from it)\n"
+ "(caution: sending requires being able to bind to a local source port)\n"
"* " ENV_SCOPED_NAME ": actions denied on the outside of the landlock domain\n"
" - \"a\" to restrict opening abstract unix sockets\n"
" - \"s\" to restrict sending signals\n"
@@ -336,6 +343,7 @@ static const char help[] =
ENV_FS_RW_NAME "=\"/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp\" "
ENV_TCP_BIND_NAME "=\"9418\" "
ENV_TCP_CONNECT_NAME "=\"80:443\" "
+ ENV_UDP_CONNECT_SEND_NAME "=\"53\" "
ENV_SCOPED_NAME "=\"a:s\" "
"%1$s bash -i\n"
"\n"
@@ -356,7 +364,9 @@ int main(const int argc, char *const argv[], char *const *const envp)
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = access_fs_rw,
.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ LANDLOCK_ACCESS_NET_CONNECT_TCP |
+ LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
LANDLOCK_SCOPE_SIGNAL,
};
@@ -444,6 +454,13 @@ int main(const int argc, char *const argv[], char *const *const envp)
/* Removes LANDLOCK_ACCESS_FS_RESOLVE_UNIX for ABI < 9 */
ruleset_attr.handled_access_fs &=
~LANDLOCK_ACCESS_FS_RESOLVE_UNIX;
+ __attribute__((fallthrough));
+ case 9:
+ /* Removes UDP support for ABI < 10 */
+ ruleset_attr.handled_access_net &=
+ ~(LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
+
/* Must be printed for any ABI < LANDLOCK_ABI_LAST. */
fprintf(stderr,
"Hint: You should update the running kernel "
@@ -475,6 +492,18 @@ int main(const int argc, char *const argv[], char *const *const envp)
ruleset_attr.handled_access_net &=
~LANDLOCK_ACCESS_NET_CONNECT_TCP;
}
+ /* Removes UDP bind access control if not supported by a user. */
+ env_port_name = getenv(ENV_UDP_BIND_NAME);
+ if (!env_port_name) {
+ ruleset_attr.handled_access_net &=
+ ~LANDLOCK_ACCESS_NET_BIND_UDP;
+ }
+ /* Removes UDP connect/send access control if not supported by a user. */
+ env_port_name = getenv(ENV_UDP_CONNECT_SEND_NAME);
+ if (!env_port_name) {
+ ruleset_attr.handled_access_net &=
+ ~LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP;
+ }
if (check_ruleset_scope(ENV_SCOPED_NAME, &ruleset_attr))
return 1;
@@ -519,6 +548,14 @@ int main(const int argc, char *const argv[], char *const *const envp)
LANDLOCK_ACCESS_NET_CONNECT_TCP)) {
goto err_close_ruleset;
}
+ if (populate_ruleset_net(ENV_UDP_BIND_NAME, ruleset_fd,
+ LANDLOCK_ACCESS_NET_BIND_UDP)) {
+ goto err_close_ruleset;
+ }
+ if (populate_ruleset_net(ENV_UDP_CONNECT_SEND_NAME, ruleset_fd,
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP)) {
+ goto err_close_ruleset;
+ }
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
perror("Failed to restrict privileges");
--
2.47.3
^ permalink raw reply related
* [PATCH v5 4/6] selftests/landlock: Add tests for UDP send
From: Matthieu Buffet @ 2026-06-11 16:21 UTC (permalink / raw)
To: Mickaël Salaün, Günther Noack
Cc: linux-security-module, Mikhail Ivanov, konstantin.meskhidze,
Tingmao Wang, netdev, Matthieu Buffet
In-Reply-To: <20260611162107.49278-1-matthieu@buffet.re>
Add tests specific to UDP sendmsg() in the protocol_* variants to ensure
behaviour is consistent across AF_INET, AF_INET6 and AF_UNIX.
Signed-off-by: Matthieu Buffet <matthieu@buffet.re>
---
tools/testing/selftests/landlock/net_test.c | 653 +++++++++++++++++++-
1 file changed, 652 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c
index bbfecd999b32..05b41e4da28f 100644
--- a/tools/testing/selftests/landlock/net_test.c
+++ b/tools/testing/selftests/landlock/net_test.c
@@ -289,9 +289,163 @@ static int connect_variant(const int sock_fd,
return connect_variant_addrlen(sock_fd, srv, get_addrlen(srv, false));
}
+static int sendto_variant_addrlen(const int sock_fd,
+ const struct service_fixture *const srv,
+ const socklen_t addrlen, void *buf,
+ size_t len, size_t flags)
+{
+ const struct sockaddr *dst = NULL;
+ ssize_t ret;
+
+ /*
+ * We never want our processes to be killed by SIGPIPE: we check
+ * return codes and errno, so that we have actual error messages.
+ */
+ flags |= MSG_NOSIGNAL;
+
+ if (srv != NULL) {
+ switch (srv->protocol.domain) {
+ case AF_UNSPEC:
+ case AF_INET:
+ dst = (const struct sockaddr *)&srv->ipv4_addr;
+ break;
+
+ case AF_INET6:
+ dst = (const struct sockaddr *)&srv->ipv6_addr;
+ break;
+
+ case AF_UNIX:
+ dst = (const struct sockaddr *)&srv->unix_addr;
+ break;
+
+ default:
+ errno = EAFNOSUPPORT;
+ return -errno;
+ }
+ }
+
+ ret = sendto(sock_fd, buf, len, flags, dst, addrlen);
+ if (ret < 0)
+ return -errno;
+
+ /* errno is not set in cases of partial writes. */
+ if (ret != len)
+ return -EINTR;
+
+ return 0;
+}
+
+static int sendto_variant(const int sock_fd,
+ const struct service_fixture *const srv, void *buf,
+ size_t len, size_t flags)
+{
+ socklen_t addrlen = 0;
+
+ if (srv != NULL)
+ addrlen = get_addrlen(srv, false);
+
+ return sendto_variant_addrlen(sock_fd, srv, addrlen, buf, len, flags);
+}
+
+static int test_sendmsg(struct __test_metadata *const _metadata,
+ const struct protocol_variant *prot, int client_fd,
+ int server_fd, const struct service_fixture *srv,
+ bool bind_denied, bool send_denied)
+{
+ int ret;
+ socklen_t opt_len;
+ int sock_type;
+ int addr_family;
+ struct sockaddr_storage peer_addr = { 0 };
+ bool has_remote_port;
+ bool needs_autobind;
+ char read_buf[1] = { 0 };
+
+ /*
+ * Prepare the test by inspecting the socket type and whether it
+ * has a local/remote address set (all of which determine the
+ * expected outcomes).
+ */
+ opt_len = sizeof(sock_type);
+ ASSERT_EQ(0, getsockopt(client_fd, SOL_SOCKET, SO_TYPE, &sock_type,
+ &opt_len));
+ opt_len = sizeof(addr_family);
+ ASSERT_EQ(0, getsockopt(client_fd, SOL_SOCKET, SO_DOMAIN, &addr_family,
+ &opt_len));
+ opt_len = sizeof(peer_addr);
+ has_remote_port = (getpeername(client_fd, (struct sockaddr *)&peer_addr,
+ &opt_len) == 0);
+ needs_autobind = (addr_family == AF_INET || addr_family == AF_INET6) &&
+ get_binded_port(client_fd, prot) == 0;
+
+ /* First, check error code with truncated explicit address. */
+ if (srv != NULL) {
+ ret = sendto_variant_addrlen(
+ client_fd, srv, get_addrlen(srv, true) - 1, "A", 1, 0);
+ if (sock_type == SOCK_STREAM && !has_remote_port) {
+ EXPECT_EQ(-EPIPE, ret)
+ {
+ return -1;
+ }
+ } else if (bind_denied && needs_autobind) {
+ EXPECT_EQ(-EACCES, ret)
+ {
+ return -1;
+ }
+ } else {
+ EXPECT_EQ(-EINVAL, ret)
+ {
+ return -1;
+ }
+ }
+ }
+
+ /* With or without explicit destination address (srv can be NULL). */
+ ret = sendto_variant(client_fd, srv, "B", 1, 0);
+ if (sock_type == SOCK_STREAM && !has_remote_port) {
+ EXPECT_EQ(-EPIPE, ret)
+ {
+ return -1;
+ }
+ } else if ((send_denied && srv != NULL) ||
+ (bind_denied && needs_autobind)) {
+ ASSERT_EQ(-EACCES, ret)
+ {
+ return -1;
+ }
+ } else if (srv == NULL && !has_remote_port) {
+ if (addr_family == AF_UNIX) {
+ ASSERT_EQ(-ENOTCONN, ret)
+ {
+ return -1;
+ }
+ } else if (sock_type == SOCK_STREAM) {
+ ASSERT_EQ(-EPIPE, ret)
+ {
+ return -1;
+ }
+ } else {
+ ASSERT_EQ(-EDESTADDRREQ, ret)
+ {
+ return -1;
+ }
+ }
+ } else {
+ ASSERT_EQ(0, ret);
+ ASSERT_EQ(1, recv(server_fd, read_buf, 1, 0));
+ ASSERT_EQ(read_buf[0], 'B')
+ {
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
FIXTURE(protocol)
{
- struct service_fixture srv0, srv1, srv2, unspec_any0, unspec_srv0;
+ struct service_fixture srv0, srv1, srv2;
+ struct service_fixture unspec_any0, unspec_srv0, unspec_srv1;
};
FIXTURE_VARIANT(protocol)
@@ -313,6 +467,7 @@ FIXTURE_SETUP(protocol)
ASSERT_EQ(0, set_service(&self->srv2, variant->prot, 2));
ASSERT_EQ(0, set_service(&self->unspec_srv0, prot_unspec, 0));
+ ASSERT_EQ(0, set_service(&self->unspec_srv1, prot_unspec, 1));
ASSERT_EQ(0, set_service(&self->unspec_any0, prot_unspec, 0));
self->unspec_any0.ipv4_addr.sin_addr.s_addr = htonl(INADDR_ANY);
@@ -1126,6 +1281,441 @@ TEST_F(protocol, connect_unspec)
EXPECT_EQ(0, close(bind_fd));
}
+TEST_F(protocol, sendmsg_stream)
+{
+ int srv0_fd, tmp_fd, client_fd, res;
+ char read_buf[1] = { 0 };
+
+ /*
+ * Simple test for stream sockets: just deny all connect()/
+ * send(explicit addr)/bind(), and make sure we don't interfere
+ * with any operation.
+ */
+ if (variant->prot.type != SOCK_STREAM)
+ return;
+
+ if (variant->sandbox == UDP_SANDBOX) {
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net =
+ LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
+ };
+ const int ruleset_fd = landlock_create_ruleset(
+ &ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+ }
+
+ ASSERT_LE(0, client_fd = socket_variant(&self->srv0));
+ ASSERT_LE(0, srv0_fd = socket_variant(&self->srv0));
+ ASSERT_EQ(0, bind_variant(srv0_fd, &self->srv0));
+ ASSERT_EQ(0, listen(srv0_fd, backlog));
+
+ /* Send on a non-connected socket. */
+ res = sendto_variant(client_fd, NULL, "A", 1, 0);
+ if (variant->prot.domain == AF_UNIX) {
+ EXPECT_EQ(-ENOTCONN, res);
+ } else {
+ EXPECT_EQ(-EPIPE, res);
+ }
+
+ /* Send to a truncated (invalid) address on a non-connected socket. */
+ res = sendto_variant_addrlen(client_fd, &self->srv0,
+ get_addrlen(&self->srv0, true) - 1, "B", 1,
+ 0);
+ if (variant->prot.domain == AF_UNIX) {
+ EXPECT_EQ(-EOPNOTSUPP, res);
+ } else {
+ EXPECT_EQ(-EPIPE, res);
+ }
+
+ /* Connect. */
+ ASSERT_EQ(0, connect_variant(client_fd, &self->srv0));
+ tmp_fd = accept(srv0_fd, NULL, 0);
+ ASSERT_LE(0, tmp_fd);
+ EXPECT_EQ(0, close(srv0_fd));
+ srv0_fd = tmp_fd;
+
+ /* Send without an explicit address. */
+ EXPECT_EQ(0, sendto_variant(client_fd, NULL, "C", 1, 0));
+ EXPECT_EQ(1, recv(srv0_fd, read_buf, 1, 0))
+ {
+ TH_LOG("recv() failed: %s", strerror(errno));
+ }
+ EXPECT_EQ(read_buf[0], 'C');
+
+ /* Send to a truncated (invalid) address. */
+ res = sendto_variant_addrlen(client_fd, &self->srv0,
+ get_addrlen(&self->srv0, true) - 1, "D", 1,
+ 0);
+ if (variant->prot.domain == AF_UNIX) {
+ EXPECT_EQ(-EISCONN, res);
+ } else {
+ ASSERT_EQ(0, res);
+ EXPECT_EQ(1, recv(srv0_fd, read_buf, 1, 0))
+ {
+ TH_LOG("recv() failed: %s", strerror(errno));
+ }
+ EXPECT_EQ(read_buf[0], 'D');
+ }
+
+ /* Send to a valid but different address. */
+ res = sendto_variant(client_fd, &self->srv1, "E", 1, 0);
+ if (variant->prot.domain == AF_UNIX) {
+ EXPECT_EQ(-EISCONN, res);
+ } else {
+ ASSERT_EQ(0, res);
+ EXPECT_EQ(1, recv(srv0_fd, read_buf, 1, 0))
+ {
+ TH_LOG("recv() failed: %s", strerror(errno));
+ }
+ EXPECT_EQ(read_buf[0], 'E');
+ }
+
+ EXPECT_EQ(0, close(client_fd));
+}
+
+TEST_F(protocol, sendmsg_dgram)
+{
+ const bool restricted = is_restricted(&variant->prot, variant->sandbox);
+ int srv0_fd, srv1_fd, client_fd, child, status, res;
+
+ if (variant->prot.type != SOCK_DGRAM)
+ return;
+
+ /* Prepare server on port #0 to be allowed. */
+ ASSERT_LE(0, srv0_fd = socket_variant(&self->srv0));
+ ASSERT_EQ(0, bind_variant(srv0_fd, &self->srv0));
+
+ /* And another server on port #1 to be denied. */
+ ASSERT_LE(0, srv1_fd = socket_variant(&self->srv1));
+ ASSERT_EQ(0, bind_variant(srv1_fd, &self->srv1));
+
+ /*
+ * Check that sockets connected before restrictions are not
+ * impacted in any way.
+ */
+ child = fork();
+ ASSERT_LE(0, child);
+ if (child == 0) {
+ ASSERT_LE(0, client_fd = socket_variant(&self->srv0));
+ ASSERT_EQ(0, connect_variant(client_fd, &self->srv0));
+ if (variant->sandbox == UDP_SANDBOX) {
+ /* Deny all connect()/send(explicit addr)/bind(). */
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net =
+ LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
+ };
+ const int ruleset_fd = landlock_create_ruleset(
+ &ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+ }
+ EXPECT_EQ(0,
+ test_sendmsg(_metadata, &variant->prot, client_fd,
+ srv0_fd, NULL, restricted, restricted));
+ EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd,
+ srv0_fd, &self->srv0, restricted,
+ restricted));
+ EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd,
+ srv1_fd, &self->srv1, restricted,
+ restricted));
+ EXPECT_EQ(0, close(client_fd));
+ _exit(_metadata->exit_code);
+ }
+ EXPECT_EQ(child, waitpid(child, &status, 0));
+ EXPECT_EQ(1, WIFEXITED(status));
+ EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+
+ /*
+ * Restrict connect/send, but not bind(). Then try sending with
+ * no destination (and no remote peer set), an allowed
+ * destination, then a denied destination.
+ */
+ child = fork();
+ ASSERT_LE(0, child);
+ if (child == 0) {
+ if (variant->sandbox == UDP_SANDBOX) {
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net =
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
+ };
+ const struct landlock_net_port_attr send_p0 = {
+ .allowed_access =
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
+ .port = self->srv0.port,
+ };
+ const int 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,
+ &send_p0, 0));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+ }
+ ASSERT_LE(0, client_fd = socket_variant(&self->srv0));
+ EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd,
+ -1, NULL, false, false));
+ EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd,
+ srv0_fd, &self->srv0, false, false));
+ EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd,
+ srv1_fd, &self->srv1, false,
+ restricted));
+ EXPECT_EQ(0, close(client_fd));
+ _exit(_metadata->exit_code);
+ return;
+ }
+ EXPECT_EQ(child, waitpid(child, &status, 0));
+ EXPECT_EQ(1, WIFEXITED(status));
+ EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+
+ /*
+ * Rest of this test is just for autobind enforcement, which only
+ * exists in IP sockets.
+ */
+ if (variant->prot.domain != AF_INET && variant->prot.domain != AF_INET6)
+ return;
+
+ /* Restrict bind() to explicit calls with an arbitrary (non-0) port. */
+ child = fork();
+ ASSERT_LE(0, child);
+ if (child == 0) {
+ const uint16_t allowed_src_port = 42424;
+ struct service_fixture allowed_src;
+
+ allowed_src = self->srv0;
+ set_port(&allowed_src, allowed_src_port);
+ if (variant->sandbox == UDP_SANDBOX) {
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net =
+ LANDLOCK_ACCESS_NET_BIND_UDP,
+ };
+ const struct landlock_net_port_attr rule = {
+ .allowed_access = LANDLOCK_ACCESS_NET_BIND_UDP,
+ .port = allowed_src_port,
+ };
+ const int 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,
+ &rule, 0));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+ }
+ ASSERT_LE(0, client_fd = socket_variant(&self->srv0));
+
+ /* Check that implicit bind(0) in sendmsg() is denied. */
+ EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd,
+ srv0_fd, &self->srv0, restricted,
+ false));
+
+ /* Same thing for autobind in connect(). */
+ res = connect_variant(client_fd, &self->srv0);
+ if (restricted) {
+ EXPECT_EQ(-EACCES, res);
+ } else {
+ EXPECT_EQ(0, res);
+ }
+ EXPECT_EQ(0, close(client_fd));
+
+ /* Make sendmsg() work by explicitly binding to the only allowed port. */
+ ASSERT_LE(0, client_fd = socket_variant(&self->srv0));
+ EXPECT_EQ(0, bind_variant(client_fd, &allowed_src));
+ EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd,
+ srv0_fd, &self->srv0, restricted,
+ false));
+ EXPECT_EQ(0, close(client_fd));
+
+ /* Make connect() work by explicitly binding to the only allowed port. */
+ ASSERT_LE(0, client_fd = socket_variant(&self->srv0));
+ EXPECT_EQ(0, bind_variant(client_fd, &allowed_src));
+ EXPECT_EQ(0, connect_variant(client_fd, &self->srv0));
+ EXPECT_EQ(0, close(client_fd));
+
+ _exit(_metadata->exit_code);
+ return;
+ }
+ EXPECT_EQ(child, waitpid(child, &status, 0));
+ EXPECT_EQ(1, WIFEXITED(status));
+ EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+
+ /*
+ * Check that %LANDLOCK_ACCESS_NET_BIND_UDP on port 0 allows
+ * implicit autobinds.
+ */
+ child = fork();
+ ASSERT_LE(0, child);
+ if (child == 0) {
+ if (variant->sandbox == UDP_SANDBOX) {
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net =
+ LANDLOCK_ACCESS_NET_BIND_UDP,
+ };
+ const struct landlock_net_port_attr rule = {
+ .allowed_access = LANDLOCK_ACCESS_NET_BIND_UDP,
+ .port = 0,
+ };
+ const int 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,
+ &rule, 0));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+ }
+ ASSERT_LE(0, client_fd = socket_variant(&self->srv0));
+ EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd,
+ srv0_fd, &self->srv0, false, false));
+ EXPECT_EQ(0, close(client_fd));
+ _exit(_metadata->exit_code);
+ }
+ EXPECT_EQ(child, waitpid(child, &status, 0));
+ EXPECT_EQ(1, WIFEXITED(status));
+ EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+}
+
+TEST_F(protocol, sendmsg_unspec)
+{
+ const bool restricted = is_restricted(&variant->prot, variant->sandbox);
+ int client_fd, srv0_fd, srv1_fd, res;
+ char read_buf[1] = { 0 };
+
+ /*
+ * We already test for the absence of influence on sendmsg for
+ * other socket types and other address families, there's no
+ * point in adapting this test for stream sockets too.
+ */
+ if (variant->prot.type != SOCK_DGRAM)
+ return;
+
+ /* Prepare client of the right family. */
+ ASSERT_LE(0, client_fd = socket_variant(&self->srv0));
+
+ /* Prepare server on port #0 to be allowed. */
+ ASSERT_LE(0, srv0_fd = socket_variant(&self->srv0));
+ ASSERT_EQ(0, bind_variant(srv0_fd, &self->srv0));
+
+ /* And another server on port #1 to be denied. */
+ ASSERT_LE(0, srv1_fd = socket_variant(&self->srv1));
+ ASSERT_EQ(0, bind_variant(srv1_fd, &self->srv1));
+
+ if (variant->sandbox == UDP_SANDBOX) {
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net =
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
+ };
+ const struct landlock_net_port_attr rule = {
+ .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
+ .port = self->srv0.port,
+ };
+ const int 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,
+ &rule, 0));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+ }
+
+ /* Explicit AF_UNSPEC address but truncated. */
+ EXPECT_EQ(-EINVAL, sendto_variant_addrlen(
+ client_fd, &self->unspec_srv0,
+ get_addrlen(&self->unspec_srv0, true) - 1,
+ "A", 1, 0));
+
+ /*
+ * Explicit AF_UNSPEC address, should be treated as AF_INET by
+ * IPv4 sockets (and thus map to srv0, allowed), but be denied by
+ * IPv6 sockets.
+ */
+ res = sendto_variant(client_fd, &self->unspec_srv0, "B", 1, 0);
+ if (variant->prot.domain == AF_INET6) {
+ if (restricted) {
+ /* Always denied on IPv6 socket. */
+ EXPECT_EQ(-EACCES, res);
+ } else {
+ /* IPv6 sockets treat AF_UNSPEC as a NULL address. */
+ EXPECT_EQ(-EDESTADDRREQ, res);
+ }
+ } else if (variant->prot.domain == AF_INET) {
+ ASSERT_EQ(0, res);
+ EXPECT_EQ(1, read(srv0_fd, read_buf, 1))
+ {
+ TH_LOG("read() failed: %s", strerror(errno));
+ }
+ EXPECT_EQ(read_buf[0], 'B');
+ } else {
+ /* Unix sockets don't accept AF_UNSPEC. */
+ EXPECT_EQ(-EINVAL, res);
+ }
+
+ /*
+ * Explicit AF_UNSPEC address, should be treated as AF_INET on
+ * IPv4 sockets (and thus map to srv1, denied), and be denied
+ * on IPv6 sockets as always.
+ */
+ res = sendto_variant(client_fd, &self->unspec_srv1, "C", 1, 0);
+ if (variant->prot.domain == AF_INET6) {
+ if (restricted) {
+ /* Always denied on IPv6 socket. */
+ EXPECT_EQ(-EACCES, res);
+ } else {
+ /* IPv6 sockets treat AF_UNSPEC as a NULL address. */
+ EXPECT_EQ(-EDESTADDRREQ, res);
+ }
+ } else if (variant->prot.domain == AF_INET) {
+ if (restricted) {
+ /* Sending to srv1 is not allowed, only srv0. */
+ EXPECT_EQ(-EACCES, res);
+ } else {
+ ASSERT_EQ(0, res);
+ EXPECT_EQ(1, read(srv1_fd, read_buf, 1))
+ {
+ TH_LOG("read() failed: %s", strerror(errno));
+ }
+ EXPECT_EQ(read_buf[0], 'C');
+ }
+ } else {
+ /* Unix sockets don't accept AF_UNSPEC. */
+ EXPECT_EQ(-EINVAL, res);
+ }
+
+ ASSERT_EQ(0, connect_variant(client_fd, &self->srv0));
+
+ /* Minimal explicit AF_UNSPEC address (just the sa_family_t field) */
+ res = sendto_variant_addrlen(client_fd, &self->unspec_srv0,
+ get_addrlen(&self->unspec_srv0, true), "D",
+ 1, 0);
+ if (variant->prot.domain == AF_INET6) {
+ if (restricted) {
+ /* AF_UNSPEC is always denied in IPv6. */
+ EXPECT_EQ(-EACCES, res);
+ } else {
+ /*
+ * IPv6 sockets treat AF_UNSPEC as a NULL address,
+ * falling back to the connected address.
+ */
+ ASSERT_EQ(0, res);
+ EXPECT_EQ(1, read(srv0_fd, read_buf, 1));
+ EXPECT_EQ(read_buf[0], 'D');
+ }
+ } else {
+ /*
+ * IPv4 socket will expect a struct sockaddr_in, our address
+ * is considered truncated.
+ * And Unix sockets don't accept AF_UNSPEC at all.
+ */
+ EXPECT_EQ(-EINVAL, res);
+ }
+}
+
FIXTURE(ipv4)
{
struct service_fixture srv0, srv1;
@@ -2187,6 +2777,7 @@ FIXTURE(audit)
{
struct service_fixture srv0;
struct service_fixture srv1;
+ struct service_fixture unspec_srv0;
struct audit_filter audit_filter;
int audit_fd;
};
@@ -2239,8 +2830,13 @@ FIXTURE_VARIANT_ADD(audit, ipv6_udp) {
FIXTURE_SETUP(audit)
{
+ struct protocol_variant prot_unspec = variant->prot;
+
+ prot_unspec.domain = AF_UNSPEC;
+
ASSERT_EQ(0, set_service(&self->srv0, variant->prot, 0));
ASSERT_EQ(0, set_service(&self->srv1, variant->prot, 1));
+ ASSERT_EQ(0, set_service(&self->unspec_srv0, prot_unspec, 0));
setup_loopback(_metadata);
@@ -2347,4 +2943,59 @@ TEST_F(audit, connect)
EXPECT_EQ(0, close(sock_fd));
}
+TEST_F(audit, sendmsg)
+{
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP |
+ LANDLOCK_ACCESS_NET_BIND_UDP,
+ };
+ const struct landlock_net_port_attr rule = {
+ .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
+ .port = self->srv1.port,
+ };
+ struct audit_records records;
+ int ruleset_fd;
+ int sock_fd;
+
+ /* Sendmsg on stream sockets is never denied. */
+ if (variant->prot.type != SOCK_DGRAM)
+ return;
+
+ 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,
+ &rule, 0));
+ 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, sendto_variant(sock_fd, &self->srv0, "A", 1, 0));
+ EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.connect_send_udp",
+ "daddr", variant->addr, "dest"));
+
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(1, records.domain);
+
+ /* Check that autobind generates a denied bind event. */
+ EXPECT_EQ(-EACCES, sendto_variant(sock_fd, &self->srv1, "A", 1, 0));
+ EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.bind_udp", NULL,
+ NULL, NULL));
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(0, records.domain);
+
+ EXPECT_EQ(-EACCES,
+ sendto_variant(sock_fd, &self->unspec_srv0, "B", 1, 0));
+ EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.connect_send_udp",
+ "daddr", NULL, "dest"));
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(0, records.domain);
+
+ EXPECT_EQ(0, close(sock_fd));
+}
+
TEST_HARNESS_MAIN
--
2.47.3
^ permalink raw reply related
* [PATCH v5 3/6] selftests/landlock: Add tests for UDP bind/connect
From: Matthieu Buffet @ 2026-06-11 16:21 UTC (permalink / raw)
To: Mickaël Salaün, Günther Noack
Cc: linux-security-module, Mikhail Ivanov, konstantin.meskhidze,
Tingmao Wang, netdev, Matthieu Buffet
In-Reply-To: <20260611162107.49278-1-matthieu@buffet.re>
Make basic changes to the existing bind() and connect() test suite to
cover UDP restriction.
Signed-off-by: Matthieu Buffet <matthieu@buffet.re>
---
tools/testing/selftests/landlock/net_test.c | 507 ++++++++++++++++----
1 file changed, 413 insertions(+), 94 deletions(-)
diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c
index 016c7277e370..bbfecd999b32 100644
--- a/tools/testing/selftests/landlock/net_test.c
+++ b/tools/testing/selftests/landlock/net_test.c
@@ -35,6 +35,7 @@ enum sandbox_type {
NO_SANDBOX,
/* This may be used to test rules that allow *and* deny accesses. */
TCP_SANDBOX,
+ UDP_SANDBOX,
};
static int set_service(struct service_fixture *const srv,
@@ -93,23 +94,53 @@ static bool prot_is_tcp(const struct protocol_variant *const prot)
(prot->protocol == IPPROTO_TCP || prot->protocol == IPPROTO_IP);
}
+static bool prot_is_udp(const struct protocol_variant *const prot)
+{
+ return (prot->domain == AF_INET || prot->domain == AF_INET6) &&
+ prot->type == SOCK_DGRAM &&
+ (prot->protocol == IPPROTO_UDP || prot->protocol == IPPROTO_IP);
+}
+
static bool is_restricted(const struct protocol_variant *const prot,
const enum sandbox_type sandbox)
{
if (sandbox == TCP_SANDBOX)
return prot_is_tcp(prot);
+ else if (sandbox == UDP_SANDBOX)
+ return prot_is_udp(prot);
return false;
}
static int socket_variant(const struct service_fixture *const srv)
{
+ /* Arbitrary value just to not block other tests indefinitely. */
+ const struct timeval timeout = {
+ .tv_sec = 0,
+ .tv_usec = 100000,
+ };
+ int sockfd;
int ret;
- ret = socket(srv->protocol.domain, srv->protocol.type | SOCK_CLOEXEC,
- srv->protocol.protocol);
- if (ret < 0)
+ sockfd = socket(srv->protocol.domain, srv->protocol.type | SOCK_CLOEXEC,
+ srv->protocol.protocol);
+ if (sockfd < 0)
return -errno;
- return ret;
+
+ ret = setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout,
+ sizeof(timeout));
+ if (ret != 0) {
+ ret = -errno;
+ close(sockfd);
+ return ret;
+ }
+ ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout,
+ sizeof(timeout));
+ if (ret != 0) {
+ ret = -errno;
+ close(sockfd);
+ return ret;
+ }
+ return sockfd;
}
#ifndef SIN6_LEN_RFC2133
@@ -271,10 +302,9 @@ FIXTURE_VARIANT(protocol)
FIXTURE_SETUP(protocol)
{
- const struct protocol_variant prot_unspec = {
- .domain = AF_UNSPEC,
- .type = SOCK_STREAM,
- };
+ struct protocol_variant prot_unspec = variant->prot;
+
+ prot_unspec.domain = AF_UNSPEC;
disable_caps(_metadata);
@@ -510,6 +540,92 @@ FIXTURE_VARIANT_ADD(protocol, tcp_sandbox_with_unix_datagram) {
},
};
+/* clang-format off */
+FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv4_udp1) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_INET,
+ .type = SOCK_DGRAM,
+ .protocol = IPPROTO_UDP,
+ },
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv4_udp2) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_INET,
+ .type = SOCK_DGRAM,
+ /* IPPROTO_IP == 0 */
+ .protocol = IPPROTO_IP,
+ },
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv6_udp1) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_INET6,
+ .type = SOCK_DGRAM,
+ .protocol = IPPROTO_UDP,
+ },
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv6_udp2) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_INET6,
+ .type = SOCK_DGRAM,
+ /* IPPROTO_IP == 0 */
+ .protocol = IPPROTO_IP,
+ },
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv4_tcp) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_INET,
+ .type = SOCK_STREAM,
+ },
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv6_tcp) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_INET6,
+ .type = SOCK_STREAM,
+ },
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_unix_stream) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_UNIX,
+ .type = SOCK_STREAM,
+ },
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_unix_datagram) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_UNIX,
+ .type = SOCK_DGRAM,
+ },
+};
+
static void test_bind_and_connect(struct __test_metadata *const _metadata,
const struct service_fixture *const srv,
const bool deny_bind, const bool deny_connect)
@@ -602,7 +718,7 @@ static void test_bind_and_connect(struct __test_metadata *const _metadata,
ret = connect_variant(connect_fd, srv);
if (deny_connect) {
EXPECT_EQ(-EACCES, ret);
- } else if (deny_bind) {
+ } else if (deny_bind && srv->protocol.type == SOCK_STREAM) {
/* No listening server. */
EXPECT_EQ(-ECONNREFUSED, ret);
} else {
@@ -641,18 +757,25 @@ static void test_bind_and_connect(struct __test_metadata *const _metadata,
TEST_F(protocol, bind)
{
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
+ const __u64 bind_access =
+ (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_BIND_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP);
+ const __u64 conn_access =
+ (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_CONNECT_TCP :
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ .handled_access_net = bind_access | conn_access,
};
- const struct landlock_net_port_attr tcp_bind_connect_p0 = {
- .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ const struct landlock_net_port_attr bind_connect_p0 = {
+ .allowed_access = bind_access | conn_access,
.port = self->srv0.port,
};
- const struct landlock_net_port_attr tcp_connect_p1 = {
- .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ const struct landlock_net_port_attr connect_p1 = {
+ .allowed_access = conn_access,
.port = self->srv1.port,
};
int ruleset_fd;
@@ -664,12 +787,26 @@ TEST_F(protocol, bind)
/* Allows connect and bind for the first port. */
ASSERT_EQ(0,
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &tcp_bind_connect_p0, 0));
+ &bind_connect_p0, 0));
/* Allows connect and denies bind for the second port. */
ASSERT_EQ(0,
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &tcp_connect_p1, 0));
+ &connect_p1, 0));
+
+ /*
+ * For UDP sockets, allows binding to ephemeral ports
+ * (required to connect or send a first datagram)
+ */
+ if (variant->sandbox == UDP_SANDBOX) {
+ const struct landlock_net_port_attr bind_ephemeral = {
+ .allowed_access = bind_access,
+ .port = 0,
+ };
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd,
+ LANDLOCK_RULE_NET_PORT,
+ &bind_ephemeral, 0));
+ }
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
@@ -691,18 +828,25 @@ TEST_F(protocol, bind)
TEST_F(protocol, connect)
{
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
+ const __u64 bind_access =
+ (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_BIND_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP);
+ const __u64 conn_access =
+ (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_CONNECT_TCP :
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ .handled_access_net = bind_access | conn_access,
};
- const struct landlock_net_port_attr tcp_bind_connect_p0 = {
- .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ const struct landlock_net_port_attr bind_connect_p0 = {
+ .allowed_access = bind_access | conn_access,
.port = self->srv0.port,
};
- const struct landlock_net_port_attr tcp_bind_p1 = {
- .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
+ const struct landlock_net_port_attr bind_p1 = {
+ .allowed_access = bind_access,
.port = self->srv1.port,
};
int ruleset_fd;
@@ -714,12 +858,26 @@ TEST_F(protocol, connect)
/* Allows connect and bind for the first port. */
ASSERT_EQ(0,
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &tcp_bind_connect_p0, 0));
+ &bind_connect_p0, 0));
/* Allows bind and denies connect for the second port. */
ASSERT_EQ(0,
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &tcp_bind_p1, 0));
+ &bind_p1, 0));
+
+ /*
+ * For UDP sockets, allows binding to ephemeral ports
+ * (required to connect or send a first datagram)
+ */
+ if (variant->sandbox == UDP_SANDBOX) {
+ const struct landlock_net_port_attr bind_ephemeral = {
+ .allowed_access = bind_access,
+ .port = 0,
+ };
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd,
+ LANDLOCK_RULE_NET_PORT,
+ &bind_ephemeral, 0));
+ }
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
@@ -737,16 +895,20 @@ TEST_F(protocol, connect)
TEST_F(protocol, bind_unspec)
{
+ const __u64 bind_access = (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_BIND_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP);
const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+ .handled_access_net = bind_access,
};
- const struct landlock_net_port_attr tcp_bind = {
- .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
+ const struct landlock_net_port_attr rule_bind = {
+ .allowed_access = bind_access,
.port = self->srv0.port,
};
int bind_fd, ret;
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
const int ruleset_fd = landlock_create_ruleset(
&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
@@ -754,7 +916,7 @@ TEST_F(protocol, bind_unspec)
/* Allows bind. */
ASSERT_EQ(0,
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &tcp_bind, 0));
+ &rule_bind, 0));
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
}
@@ -782,7 +944,8 @@ TEST_F(protocol, bind_unspec)
}
EXPECT_EQ(0, close(bind_fd));
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
const int ruleset_fd = landlock_create_ruleset(
&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
@@ -828,11 +991,21 @@ TEST_F(protocol, bind_unspec)
TEST_F(protocol, connect_unspec)
{
- const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ const __u64 connect_right =
+ (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_CONNECT_TCP :
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
+ const __u64 bind_right = (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_BIND_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP);
+ const struct landlock_ruleset_attr ruleset_conn = {
+ .handled_access_net = connect_right,
};
- const struct landlock_net_port_attr tcp_connect = {
- .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ const struct landlock_ruleset_attr ruleset_conn_bind = {
+ .handled_access_net = connect_right | bind_right,
+ };
+ const struct landlock_net_port_attr rule_connect = {
+ .allowed_access = connect_right,
.port = self->srv0.port,
};
int bind_fd, client_fd, status;
@@ -865,15 +1038,16 @@ TEST_F(protocol, connect_unspec)
EXPECT_EQ(0, ret);
}
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
const int ruleset_fd = landlock_create_ruleset(
- &ruleset_attr, sizeof(ruleset_attr), 0);
+ &ruleset_conn, sizeof(ruleset_conn), 0);
ASSERT_LE(0, ruleset_fd);
/* Allows connect. */
ASSERT_EQ(0, landlock_add_rule(ruleset_fd,
LANDLOCK_RULE_NET_PORT,
- &tcp_connect, 0));
+ &rule_connect, 0));
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
}
@@ -896,12 +1070,14 @@ TEST_F(protocol, connect_unspec)
EXPECT_EQ(0, ret);
}
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
const int ruleset_fd = landlock_create_ruleset(
- &ruleset_attr, sizeof(ruleset_attr), 0);
+ &ruleset_conn_bind, sizeof(ruleset_conn_bind),
+ 0);
ASSERT_LE(0, ruleset_fd);
- /* Denies connect. */
+ /* Denies connect and bind. */
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
}
@@ -975,6 +1151,13 @@ FIXTURE_VARIANT_ADD(ipv4, tcp_sandbox_with_tcp) {
.type = SOCK_STREAM,
};
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ipv4, udp_sandbox_with_tcp) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .type = SOCK_STREAM,
+};
+
/* clang-format off */
FIXTURE_VARIANT_ADD(ipv4, no_sandbox_with_udp) {
/* clang-format on */
@@ -989,6 +1172,13 @@ FIXTURE_VARIANT_ADD(ipv4, tcp_sandbox_with_udp) {
.type = SOCK_DGRAM,
};
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ipv4, udp_sandbox_with_udp) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .type = SOCK_DGRAM,
+};
+
FIXTURE_SETUP(ipv4)
{
const struct protocol_variant prot = {
@@ -1012,14 +1202,19 @@ TEST_F(ipv4, from_unix_to_inet)
{
int unix_stream_fd, unix_dgram_fd;
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
+ const __u64 access_rights =
+ (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_BIND_TCP |
+ LANDLOCK_ACCESS_NET_CONNECT_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ .handled_access_net = access_rights,
};
const struct landlock_net_port_attr tcp_bind_connect_p0 = {
- .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ .allowed_access = access_rights,
.port = self->srv0.port,
};
int ruleset_fd;
@@ -1680,6 +1875,7 @@ TEST_F(ipv4_tcp, with_fs)
FIXTURE(port_specific)
{
struct service_fixture srv0;
+ struct service_fixture cli1;
};
FIXTURE_VARIANT(port_specific)
@@ -1699,7 +1895,7 @@ FIXTURE_VARIANT_ADD(port_specific, no_sandbox_with_ipv4) {
};
/* clang-format off */
-FIXTURE_VARIANT_ADD(port_specific, sandbox_with_ipv4) {
+FIXTURE_VARIANT_ADD(port_specific, tcp_sandbox_with_ipv4) {
/* clang-format on */
.sandbox = TCP_SANDBOX,
.prot = {
@@ -1708,6 +1904,16 @@ FIXTURE_VARIANT_ADD(port_specific, sandbox_with_ipv4) {
},
};
+/* clang-format off */
+FIXTURE_VARIANT_ADD(port_specific, udp_sandbox_with_ipv4) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_INET,
+ .type = SOCK_DGRAM,
+ },
+};
+
/* clang-format off */
FIXTURE_VARIANT_ADD(port_specific, no_sandbox_with_ipv6) {
/* clang-format on */
@@ -1719,7 +1925,7 @@ FIXTURE_VARIANT_ADD(port_specific, no_sandbox_with_ipv6) {
};
/* clang-format off */
-FIXTURE_VARIANT_ADD(port_specific, sandbox_with_ipv6) {
+FIXTURE_VARIANT_ADD(port_specific, tcp_sandbox_with_ipv6) {
/* clang-format on */
.sandbox = TCP_SANDBOX,
.prot = {
@@ -1728,11 +1934,22 @@ FIXTURE_VARIANT_ADD(port_specific, sandbox_with_ipv6) {
},
};
+/* clang-format off */
+FIXTURE_VARIANT_ADD(port_specific, udp_sandbox_with_ipv6) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_INET6,
+ .type = SOCK_DGRAM,
+ },
+};
+
FIXTURE_SETUP(port_specific)
{
disable_caps(_metadata);
ASSERT_EQ(0, set_service(&self->srv0, variant->prot, 0));
+ ASSERT_EQ(0, set_service(&self->cli1, variant->prot, 1));
setup_loopback(_metadata);
};
@@ -1747,14 +1964,19 @@ TEST_F(port_specific, bind_connect_zero)
uint16_t port;
/* Adds a rule layer with bind and connect actions. */
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
+ const __u64 access_rights =
+ (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_BIND_TCP |
+ LANDLOCK_ACCESS_NET_CONNECT_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP
+ .handled_access_net = access_rights,
};
- const struct landlock_net_port_attr tcp_bind_connect_zero = {
- .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ const struct landlock_net_port_attr bind_connect_zero = {
+ .allowed_access = access_rights,
.port = 0,
};
int ruleset_fd;
@@ -1766,7 +1988,7 @@ TEST_F(port_specific, bind_connect_zero)
/* Checks zero port value on bind and connect actions. */
EXPECT_EQ(0,
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &tcp_bind_connect_zero, 0));
+ &bind_connect_zero, 0));
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
@@ -1787,11 +2009,16 @@ TEST_F(port_specific, bind_connect_zero)
ret = bind_variant(bind_fd, &self->srv0);
EXPECT_EQ(0, ret);
- EXPECT_EQ(0, listen(bind_fd, backlog));
+ if (variant->prot.type == SOCK_STREAM)
+ EXPECT_EQ(0, listen(bind_fd, backlog));
/* Connects on port 0. */
ret = connect_variant(connect_fd, &self->srv0);
- EXPECT_EQ(-ECONNREFUSED, ret);
+ if (variant->prot.type == SOCK_STREAM) {
+ EXPECT_EQ(-ECONNREFUSED, ret);
+ } else {
+ EXPECT_EQ(0, ret);
+ }
/* Sets binded port for both protocol families. */
port = get_binded_port(bind_fd, &variant->prot);
@@ -1815,23 +2042,35 @@ TEST_F(port_specific, bind_connect_1023)
int bind_fd, connect_fd, ret;
/* Adds a rule layer with bind and connect actions. */
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
+ const __u64 bind_right = (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_BIND_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP);
+ const __u64 access_rights =
+ (variant->sandbox == TCP_SANDBOX ?
+ (LANDLOCK_ACCESS_NET_BIND_TCP |
+ LANDLOCK_ACCESS_NET_CONNECT_TCP) :
+ (LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP));
const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP
+ .handled_access_net = access_rights,
};
/* A rule with port value less than 1024. */
- const struct landlock_net_port_attr tcp_bind_connect_low_range = {
- .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ const struct landlock_net_port_attr bind_connect_low_range = {
+ .allowed_access = access_rights,
.port = 1023,
};
/* A rule with 1024 port. */
- const struct landlock_net_port_attr tcp_bind_connect = {
- .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ const struct landlock_net_port_attr bind_connect = {
+ .allowed_access = access_rights,
.port = 1024,
};
+ /* A rule with cli1's port, to use as source port. */
+ const struct landlock_net_port_attr srcport = {
+ .allowed_access = bind_right,
+ .port = self->cli1.port,
+ };
int ruleset_fd;
ruleset_fd = landlock_create_ruleset(&ruleset_attr,
@@ -1840,10 +2079,15 @@ TEST_F(port_specific, bind_connect_1023)
ASSERT_EQ(0,
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &tcp_bind_connect_low_range, 0));
+ &bind_connect_low_range, 0));
ASSERT_EQ(0,
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &tcp_bind_connect, 0));
+ &bind_connect, 0));
+ if (variant->sandbox == UDP_SANDBOX) {
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd,
+ LANDLOCK_RULE_NET_PORT,
+ &srcport, 0));
+ }
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
@@ -1852,9 +2096,6 @@ TEST_F(port_specific, bind_connect_1023)
bind_fd = socket_variant(&self->srv0);
ASSERT_LE(0, bind_fd);
- connect_fd = socket_variant(&self->srv0);
- ASSERT_LE(0, connect_fd);
-
/* Sets address port to 1023 for both protocol families. */
set_port(&self->srv0, 1023);
/* Binds on port 1023. */
@@ -1867,8 +2108,19 @@ TEST_F(port_specific, bind_connect_1023)
ret = bind_variant(bind_fd, &self->srv0);
clear_cap(_metadata, CAP_NET_BIND_SERVICE);
EXPECT_EQ(0, ret);
- EXPECT_EQ(0, listen(bind_fd, backlog));
+ if (variant->prot.type == SOCK_STREAM)
+ EXPECT_EQ(0, listen(bind_fd, backlog));
+ connect_fd = socket_variant(&self->srv0);
+ ASSERT_LE(0, connect_fd);
+ if (variant->prot.type == SOCK_DGRAM) {
+ /*
+ * We are about to connect(), but bind() is restricted, so for
+ * UDP sockets we need to use cli1's port as source port (the
+ * only one we are allowed to use).
+ */
+ EXPECT_EQ(0, bind_variant(connect_fd, &self->cli1));
+ }
/* Connects on the binded port 1023. */
ret = connect_variant(connect_fd, &self->srv0);
EXPECT_EQ(0, ret);
@@ -1887,7 +2139,10 @@ TEST_F(port_specific, bind_connect_1023)
/* Binds on port 1024. */
ret = bind_variant(bind_fd, &self->srv0);
EXPECT_EQ(0, ret);
- EXPECT_EQ(0, listen(bind_fd, backlog));
+ if (variant->prot.type == SOCK_STREAM)
+ EXPECT_EQ(0, listen(bind_fd, backlog));
+ if (variant->prot.type == SOCK_DGRAM)
+ EXPECT_EQ(0, bind_variant(connect_fd, &self->cli1));
/* Connects on the binded port 1024. */
ret = connect_variant(connect_fd, &self->srv0);
@@ -1897,23 +2152,30 @@ TEST_F(port_specific, bind_connect_1023)
EXPECT_EQ(0, close(bind_fd));
}
-static int matches_log_tcp(const int audit_fd, const char *const blockers,
- const char *const dir_addr, const char *const addr,
- const char *const dir_port)
+static int matches_auditlog(const int audit_fd, const char *const blockers,
+ const char *const dir_addr, const char *const addr,
+ const char *const dir_port)
{
- static const char log_template[] = REGEX_LANDLOCK_PREFIX
+ static const char log_with_addrport_tmpl[] = REGEX_LANDLOCK_PREFIX
" blockers=%s %s=%s %s=1024$";
+ static const char log_without_addrport_tmpl[] = REGEX_LANDLOCK_PREFIX
+ " blockers=%s";
/*
* Max strlen(blockers): 16
* Max strlen(dir_addr): 5
* Max strlen(addr): 12
* Max strlen(dir_port): 4
*/
- char log_match[sizeof(log_template) + 37];
+ char log_match[sizeof(log_with_addrport_tmpl) + 37];
int log_match_len;
- log_match_len = snprintf(log_match, sizeof(log_match), log_template,
- blockers, dir_addr, addr, dir_port);
+ if (addr == NULL)
+ log_match_len = snprintf(log_match, sizeof(log_match),
+ log_without_addrport_tmpl, blockers);
+ else
+ log_match_len = snprintf(log_match, sizeof(log_match),
+ log_with_addrport_tmpl, blockers,
+ dir_addr, addr, dir_port);
if (log_match_len > sizeof(log_match))
return -E2BIG;
@@ -1924,6 +2186,7 @@ static int matches_log_tcp(const int audit_fd, const char *const blockers,
FIXTURE(audit)
{
struct service_fixture srv0;
+ struct service_fixture srv1;
struct audit_filter audit_filter;
int audit_fd;
};
@@ -1935,7 +2198,7 @@ FIXTURE_VARIANT(audit)
};
/* clang-format off */
-FIXTURE_VARIANT_ADD(audit, ipv4) {
+FIXTURE_VARIANT_ADD(audit, ipv4_tcp) {
/* clang-format on */
.addr = "127\\.0\\.0\\.1",
.prot = {
@@ -1945,7 +2208,17 @@ FIXTURE_VARIANT_ADD(audit, ipv4) {
};
/* clang-format off */
-FIXTURE_VARIANT_ADD(audit, ipv6) {
+FIXTURE_VARIANT_ADD(audit, ipv4_udp) {
+ /* clang-format on */
+ .addr = "127\\.0\\.0\\.1",
+ .prot = {
+ .domain = AF_INET,
+ .type = SOCK_DGRAM,
+ },
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(audit, ipv6_tcp) {
/* clang-format on */
.addr = "::1",
.prot = {
@@ -1954,9 +2227,21 @@ FIXTURE_VARIANT_ADD(audit, ipv6) {
},
};
+/* clang-format off */
+FIXTURE_VARIANT_ADD(audit, ipv6_udp) {
+ /* clang-format on */
+ .addr = "::1",
+ .prot = {
+ .domain = AF_INET6,
+ .type = SOCK_DGRAM,
+ },
+};
+
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);
@@ -1974,9 +2259,17 @@ FIXTURE_TEARDOWN(audit)
TEST_F(audit, bind)
{
+ const char *audit_evt = (variant->prot.type == SOCK_STREAM ?
+ "net\\.bind_tcp" :
+ "net\\.bind_udp");
+ const __u64 access_rights =
+ (variant->prot.type == SOCK_STREAM ?
+ LANDLOCK_ACCESS_NET_BIND_TCP |
+ LANDLOCK_ACCESS_NET_CONNECT_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ .handled_access_net = access_rights,
};
struct audit_records records;
int ruleset_fd, sock_fd;
@@ -1990,8 +2283,8 @@ TEST_F(audit, bind)
sock_fd = socket_variant(&self->srv0);
ASSERT_LE(0, sock_fd);
EXPECT_EQ(-EACCES, bind_variant(sock_fd, &self->srv0));
- EXPECT_EQ(0, matches_log_tcp(self->audit_fd, "net\\.bind_tcp", "saddr",
- variant->addr, "src"));
+ EXPECT_EQ(0, matches_auditlog(self->audit_fd, audit_evt, "saddr",
+ variant->addr, "src"));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
@@ -2002,9 +2295,22 @@ TEST_F(audit, bind)
TEST_F(audit, connect)
{
+ const char *audit_evt = (variant->prot.type == SOCK_STREAM ?
+ "net\\.connect_tcp" :
+ "net\\.connect_send_udp");
+ const __u64 bind_right = (variant->prot.type == SOCK_STREAM ?
+ LANDLOCK_ACCESS_NET_BIND_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP);
+ const __u64 conn_right = (variant->prot.type == SOCK_STREAM ?
+ LANDLOCK_ACCESS_NET_CONNECT_TCP :
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
+ const __u64 access_rights = bind_right | conn_right;
const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ .handled_access_net = access_rights,
+ };
+ const struct landlock_net_port_attr rule_connect_p1 = {
+ .allowed_access = conn_right,
+ .port = self->srv1.port,
};
struct audit_records records;
int ruleset_fd, sock_fd;
@@ -2012,19 +2318,32 @@ 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,
+ &rule_connect_p1, 0));
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_auditlog(self->audit_fd, audit_evt, "daddr",
+ variant->addr, "dest"));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
+ if (variant->prot.type == SOCK_DGRAM) {
+ /* Check that autobind generates a denied bind event. */
+ EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv1));
+
+ EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.bind_udp",
+ NULL, NULL, NULL));
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(0, records.domain);
+ }
+
EXPECT_EQ(0, close(sock_fd));
}
--
2.47.3
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox