* Re: [RFC v1 0/1] Implement IMA Event Log Trimming
From: Roberto Sassu @ 2025-11-24 10:01 UTC (permalink / raw)
To: Anirudh Venkataramanan, linux-integrity
Cc: Mimi Zohar, Roberto Sassu, Dmitry Kasatkin, Eric Snowberg,
Paul Moore, James Morris, Serge E . Hallyn, linux-security-module,
Steven Chen, Gregory Lumen, Lakshmi Ramasubramanian,
Sush Shringarputale
In-Reply-To: <a77e9609-f6bd-4e6d-88be-5422f780b496@linux.microsoft.com>
On Fri, 2025-11-21 at 11:13 -0800, Anirudh Venkataramanan wrote:
> On 11/20/2025 3:02 AM, Roberto Sassu wrote:
> > On Wed, 2025-11-19 at 13:33 -0800, Anirudh Venkataramanan wrote:
> > > ==========================================================================
> > > > A. Introduction |
> > > ==========================================================================
> > >
> > > IMA events are kept in kernel memory and preserved across kexec soft
> > > reboots. This can lead to increased kernel memory usage over time,
> > > especially with aggressive IMA policies that measure everything. To reduce
> > > memory pressure, it becomes necessary to discard IMA events but given that
> > > IMA events are extended into PCRs in the TPM, just discarding events will
> > > break the PCR extension chain, making future verification of the IMA event
> > > log impossible.
> > >
> > > This patch series proposes a method to discard IMA events while keeping the
> > > log verifiable. While reducing memory pressure is the primary objective,
> > > the second order benefit of trimming the IMA log is that IMA log verifiers
> > > (local userspace daemon or a remote cloud service) can process smaller IMA
> > > logs on a rolling basis, thus avoiding re-verification of previously
> > > verified events.
> >
> > Hi Anirudh
>
> Hi Roberto,
>
> Thanks for the feedback! Few questions below.
>
> >
> > I will rephrase this paragraph, to be sure that I understood it
> > correctly.
> >
> > You are proposing a method to trim the measurement list and, at the
> > same time, to keep the measurement list verifiable after trimming. The
> > way you would like to achieve that is to keep the verification state in
> > the kernel in the form of PCR values.
> >
> > Those values mean what verifiers have already verified. Thus, for the
> > next verification attempt, verifiers take the current PCR values as
> > starting values, replay the truncated IMA measurement list, and again
> > you match with the current PCR values and you trim until that point.
> >
> > So the benefit of this proposal is that you keep the verification of
> > the IMA measurement list self-contained by using the last verification
> > state (PCR starting value) and the truncated IMA measurement list as
> > the inputs of your verification.
>
> Your understanding as described above is correct.
>
> >
> > Let me reiterate on the trusted computing principles IMA relies on for
> > providing the evidence about a system's integrity.
> >
> > Unless you are at the beginning of the measurement chain, where the
> > Root of Trust for Measurement (RTM) is trusted by assumption, the
> > measurements done by a component can be trusted because that component
> > was already measured by the previous component in the boot chain,
> > before it had any chance to corrupt the system.
> >
> > In the context of IMA, IMA can be trusted to make new measurements
> > because it measures every file before those files could cause any harm
> > to the system. So, potentially IMA and the kernel can be corrupted by
> > any file.
> >
> > What you are proposing would not work, because you are placing trust in
> > an input (the PCR starting value) that can be manipulated at any time
> > by a corrupted kernel, before you had the chance to detect such
> > corruption.
>
> If starting PCR values can be corrupted, the IMA measurements list can
> also be corrupted, right?
Yes, my point was that there could be a malicious update of the PCR
starting value, so that the IMA measurements list with omitted entries
looks not corrupted.
> More generally, what integrity guarantees can be provided (if any) if
> the kernel itself is corrupted?
None. This is exactly the point: measuring the files before they get
accessed is the only way to provide reliable measurements. After that,
we assume that the operation can corrupt both the user space processes
and the kernel itself (the threat model takes into consideration only
regular files).
But if that happened, replaying the IMA measurements list will reveal
the attack at that point, and the remote verifier can conclude that any
measurement after that cannot be trusted.
Your solution is storing the verification state in the kernel, and make
remote verifiers rely on it for resuming their verification. But,
because the kernel can get potentially corrupted by every measurement,
the verification state can also get potentially corrupted by every
measurement, thus cannot be trusted.
The only way to make the verification of measurements list snapshots
work is that the verification state is stored outside the system to
evaluate (which can be assumed to be trusted), so that you are sure
that the system is not advancing the PCR starting value by itself.
> > Let me describe a scenario where I could take advantage of such
> > weakness. After the first measurement list trim, I perform an attack on
> > the system such that it corrupts the kernel. IMA added a new entry in
> > the measurement list, which would reveal the attack.
> >
> > But, since I have control of the kernel, I conveniently update the PCR
> > starting value to replay the new measurement entry, and remove the
> > measurement entry from the measurement list.
> >
> > Now, from the perspective of the user space verifiers everything is
> > fine, the truncated IMA measurement list is clean, no attack, and the
> > current PCR values match by replaying the new PCR starting value with
> > the remaining of the IMA measurement list.
>
> Wouldn't the verifier detect the attack when it sees that its
> recalculated PCR values don't match up to the PCR digest in the TPM quote?
I think not, because the system replayed the entries it wants to omit
by itself. If the remote verifier is trusting the PCR starting value
from the system, the remote verifier will replay the remaining
measurement entries and will obtain the PCR current values.
The same will happen if someone in the system just did a regular trim
without the remote attestation agent noticing it. Unless the agent
stored which was the last PCR starting value it took, it would not
notice.
But, again, if you rely on the remote attestation agent to maintain the
verification state locally in the system, you are assuming that the
agent will not be corrupted, which is a much stronger assumption than
just letting the agent pass data to the remote verifier (which instead
can be trusted to detect the PCR mismatch).
So, yes, the point of trimming is to just get the IMA measurements list
out of the kernel memory. I'm not opposing to trim N entries instead of
the entire IMA measurements list, as long as: (1) the PCR matching is
done in user space and is done only for convenience (can be totally
untrusted); (2) the verification state is stored in the remote verifier
(outside the system evaluated), and latter detects the PCR mismatch.
Roberto
> > So, in my opinion the kernel should just offer the ability to trim the
> > measurement list, and a remote verifier should be responsible to verify
> > the measurement list, without relying on anything from the system being
> > evaluated.
> >
> > Sure, the remote verifier can verify just the trimmed IMA measurement
> > list, but the remote verifier must solely rely on state information
> > maintained internally.
> >
> > Roberto
> >
> > > The method has other advantages too:
> > >
> > > 1. It provides a userspace interface that can be used to precisely control
> > > trim point, allowing for trim points to be optionally aligned with
> > > userspace IMA event log validation.
> > >
> > > 2. It ensures that all necessary information required for continued IMA
> > > log validation is made available via the userspace interface at all
> > > times.
> > >
> > > 3. It provides a simple mechanism for userspace applications to determine
> > > if the event log has been unexpectedly trimmed.
> > >
> > > 4. The duration for which the IMA Measurement list mutex must be held (for
> > > trimming) is minimal.
> > >
> > > ==========================================================================
> > > > B. Solution |
> > > ==========================================================================
> > >
> > > --------------------------------------------------------------------------
> > > > B.1 Overview |
> > > --------------------------------------------------------------------------
> > >
> > > The kernel trims the IMA event log based on PCR values supplied by userspace.
> > > The core principles leveraged are as follows:
> > >
> > > - Given an IMA event log, PCR values for each IMA event can be obtained by
> > > recalulating the PCR extension for each event. Thus processing N events
> > > from the start will yield PCR values as of event N. This is referred to
> > > as "IMA event log replay".
> > >
> > > - To get the PCR value for event N + 1, only the PCR value as of event N
> > > is needed. If this can be known, events till and including N can be
> > > safely purged.
> > >
> > > Putting it all together, we get the following userspace + kernel flow:
> > >
> > > 1. A userspace application replays the IMA event log to generate PCR
> > > values and then triggers a trim by providing these values to the kernel
> > > (by writing to a pseudo file).
> > >
> > > Optionally, the userspace application may verify these PCR values
> > > against the corresponding TPM quote, and trigger trimming only if
> > > the calculated PCR values match up to the expectations in the quote's
> > > PCR digest.
> > >
> > > 2. The kernel uses the userspace supplied PCR values to trim the IMA
> > > measurements list at a specific point, and so these are referred to as
> > > "trim-to PCR values" in this context.
> > >
> > > Note that the kernel doesn't really understand what these userspace
> > > provided PCR values mean or what IMA event they correspond to, and so
> > > it does its own IMA event replay till either the replayed PCR values
> > > match with the userspace provided ones, or it runs out of events.
> > >
> > > If a match is found, the kernel can proceed with trimming the IMA
> > > measurements list. This is done in two steps, to keep locking context
> > > minimal.
> > >
> > > step 1: Find and return the list entry (as a count from head) of exact
> > > match. This does not lock the measurements list mutex, ensuring
> > > new events can be appended to the log.
> > >
> > > step 2: Lock the measurements list mutex and trim the measurements list
> > > at the previously identified list entry.
> > >
> > > If the trim is successful, the trim-to PCR values are saved as "starting
> > > PCR values". The next time userspace wants to replay the IMA event log,
> > > it will use the starting PCR values as the base for the IMA event log
> > > replay.
> > >
> > > --------------------------------------------------------------------------
> > > > B.2 Kernel Interfaces |
> > > --------------------------------------------------------------------------
> > >
> > > A new configfs pseudo file /sys/kernel/config/ima/pcrs that supports the
> > > following operations is exposed.
> > >
> > > read: returns starting PCR values stored in the kernel (within IMA
> > > specifically).
> > >
> > > write: writes trim-to PCR values to trigger trimming. If trimming is
> > > successful, trim-to PCR values are stored as starting PCR values.
> > > requires root privileges.
> > >
> > > --------------------------------------------------------------------------
> > > > B.3 Walk-through with a real example |
> > > --------------------------------------------------------------------------
> > >
> > > This is a real example from a test run.
> > >
> > > Suppose this IMA policy is deployed:
> > >
> > > measure func=FILE_CHECK mask=MAY_READ pcr=10
> > > measure func=FILE_CHECK mask=MAY_WRITE pcr=11
> > >
> > > When the policy is deployed, a zero digest starting PCR value will be set
> > > for each PCR used. If the TPM supports multiple hashbanks, there will be
> > > one starting PCR value per PCR, per TPM hashbank. This can be seen in the
> > > following hexdump:
> > >
> > > $ sudo hexdump -vC /sys/kernel/config/ima/pcrs
> > > 00000000 70 63 72 31 30 3a 73 68 61 31 3a 00 00 00 00 00 |pcr10:sha1:.....|
> > > 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 70 |...............p|
> > > 00000020 63 72 31 31 3a 73 68 61 31 3a 00 00 00 00 00 00 |cr11:sha1:......|
> > > 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 70 63 |..............pc|
> > > 00000040 72 31 30 3a 73 68 61 32 35 36 3a 00 00 00 00 00 |r10:sha256:.....|
> > > 00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
> > > 00000060 00 00 00 00 00 00 00 00 00 00 00 70 63 72 31 31 |...........pcr11|
> > > 00000070 3a 73 68 61 32 35 36 3a 00 00 00 00 00 00 00 00 |:sha256:........|
> > > 00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
> > > 00000090 00 00 00 00 00 00 00 00 70 63 72 31 30 3a 73 68 |........pcr10:sh|
> > > 000000a0 61 33 38 34 3a 00 00 00 00 00 00 00 00 00 00 00 |a384:...........|
> > > 000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
> > > 000000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
> > > 000000d0 00 00 00 00 00 70 63 72 31 31 3a 73 68 61 33 38 |.....pcr11:sha38|
> > > 000000e0 34 3a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |4:..............|
> > > 000000f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
> > > 00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
> > > 00000110 00 00 |..|
> > > 00000112
> > >
> > > Let's say that a userspace utility replays the IMA event log, and triggers
> > > trimming by writing the following PCR values (i.e. trim-to PCR values) to the
> > > pseudo file:
> > >
> > > pcr10:sha256:8268782906555cf3aefc179f815c878527dd4e67eaa836572ebabab31977922c
> > > pcr11:sha256:4c7f31927183eacb53d51d95b0162916fd3fca51a8d1efc6dde3805eb891fe41
> > >
> > > The trim is successful,
> > >
> > > 1. Some number of entries from the measurements log will disappear. This
> > > can be verified by reading out the ASCII or binary IMA measurements
> > > file.
> > >
> > > 2. The trim-to PCR values are saved as starting PCR values. This can be
> > > verified by reading out the pseudo file again as shown below. Note that
> > > even through only sha256 PCR values were written, the kernel populated
> > > sha1 and sha384 starting values as well.
> > >
> > > $ sudo hexdump -vC /sys/kernel/config/ima/pcrs
> > >
> > > 00000000 70 63 72 31 30 3a 73 68 61 31 3a c4 7f 9d 00 68 |pcr10:sha1:....h|
> > > 00000010 e4 86 71 bf bc ae f0 10 12 ff 68 e2 9e 74 e4 70 |..q.......h..t.p|
> > > 00000020 63 72 31 31 3a 73 68 61 31 3a 90 d7 17 ac 60 4d |cr11:sha1:....`M|
> > > 00000030 c8 25 ce 77 7d 9d 94 cf 44 7b b2 2e 2e e2 70 63 |.%.w}...D{....pc|
> > > 00000040 72 31 30 3a 73 68 61 32 35 36 3a 82 68 78 29 06 |r10:sha256:.hx).|
> > > 00000050 55 5c f3 ae fc 17 9f 81 5c 87 85 27 dd 4e 67 ea |U\......\..'.Ng.|
> > > 00000060 a8 36 57 2e ba ba b3 19 77 92 2c 70 63 72 31 31 |.6W.....w.,pcr11|
> > > 00000070 3a 73 68 61 32 35 36 3a 4c 7f 31 92 71 83 ea cb |:sha256:L.1.q...|
> > > 00000080 53 d5 1d 95 b0 16 29 16 fd 3f ca 51 a8 d1 ef c6 |S.....)..?.Q....|
> > > 00000090 dd e3 80 5e b8 91 fe 41 70 63 72 31 30 3a 73 68 |...^...Apcr10:sh|
> > > 000000a0 61 33 38 34 3a 8e d6 12 18 b1 d6 cd 95 16 98 33 |a384:..........3|
> > > 000000b0 2b 7d a2 d6 d9 05 c7 e8 5b 15 b0 91 c5 fc 23 d1 |+}......[.....#.|
> > > 000000c0 f9 a8 8d 60 50 5c e9 64 5f d7 b3 b2 f1 9c 90 0a |...`P\.d_.......|
> > > 000000d0 45 53 5d b2 57 70 63 72 31 31 3a 73 68 61 33 38 |ES].Wpcr11:sha38|
> > > 000000e0 34 3a 25 fc 21 28 31 5a f7 c6 fb 0f 40 c9 06 e6 |4:%.!(1Z....@...|
> > > 000000f0 c5 da ed 20 61 a1 03 54 4f 67 18 88 82 0f 48 d1 |... a..TOg....H.|
> > > 00000100 2f e0 3d 36 46 5e 94 a4 88 51 f8 91 39 7e e5 97 |/.=6F^...Q..9~..|
> > > 00000110 2c c5 |,.|
> > > 00000112
> > >
> > > --------------------------------------------------------------------------
> > > > C. Footnotes |
> > > --------------------------------------------------------------------------
> > >
> > > 1. The 'pcrs' pseudo file is currently part of configfs. This was due to
> > > some early internal feedback in a different context. This can as well be
> > > in securityfs with the rest of the IMA pseudo files.
> > >
> > > 2. PCR values are never read out of the TPM at any point. All PCR values
> > > used are derived from IMA event log replay.
> > >
> > > 3. Code is "RFC quality". Refinements can be made if the method is accepted.
> > >
> > > 4. For functional validation, base kernel version was 6.17 stable, with the
> > > most recent tested version being 6.17.8.
> > >
> > > 5. Code has been validated to some degree using a python-based internal test
> > > tool. This can be published if there is community interest.
> > >
> > > Steven Chen (1):
> > > ima: Implement IMA event log trimming
> > >
> > > drivers/Kconfig | 2 +
> > > drivers/Makefile | 1 +
> > > drivers/ima/Kconfig | 13 +
> > > drivers/ima/Makefile | 2 +
> > > drivers/ima/ima_config_pcrs.c | 291 ++++++++++++++++++
> > > include/linux/ima.h | 27 ++
> > > security/integrity/ima/Makefile | 4 +
> > > security/integrity/ima/ima.h | 8 +
> > > security/integrity/ima/ima_init.c | 44 +++
> > > security/integrity/ima/ima_log_trim.c | 421 ++++++++++++++++++++++++++
> > > security/integrity/ima/ima_policy.c | 7 +-
> > > security/integrity/ima/ima_queue.c | 5 +-
> > > 12 files changed, 821 insertions(+), 4 deletions(-)
> > > create mode 100644 drivers/ima/Kconfig
> > > create mode 100644 drivers/ima/Makefile
> > > create mode 100644 drivers/ima/ima_config_pcrs.c
> > > create mode 100644 security/integrity/ima/ima_log_trim.c
> > >
> >
>
^ permalink raw reply
* Re: [PATCH v5 07/10] selftests/landlock: add tests for quiet flag with fs rules
From: Justin Suess @ 2025-11-24 14:36 UTC (permalink / raw)
To: m; +Cc: gnoack, jack, linux-security-module, mic, utilityemal77, xandfury
In-Reply-To: <a839025f9ce9facee60ff8238ee350b5e780b777.1763931318.git.m@maowtm.org>
Good morning,
Great job on the patch.
Small suggestion on the tests and samples. I saw you
added a bool quiet to some methods for the quiet flag.
> diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
> index 943b6e2ac53d..6aa65d344c72 100644
> --- a/tools/testing/selftests/landlock/fs_test.c
> +++ b/tools/testing/selftests/landlock/fs_test.c
> @@ -718,11 +718,15 @@ TEST_F_FORK(layout1, rule_with_unhandled_access)
>
> static void add_path_beneath(struct __test_metadata *const _metadata,
> const int ruleset_fd, const __u64 allowed_access,
> - const char *const path)
> + const char *const path, bool quiet)
> {
> struct landlock_path_beneath_attr path_beneath = {
> .allowed_access = allowed_access,
> };
> + __u32 flags = 0;
> +
> + if (quiet)
> + flags |= LANDLOCK_ADD_RULE_QUIET;
>
> path_beneath.parent_fd = open(path, O_PATH | O_CLOEXEC);
> ASSERT_LE(0, path_beneath.parent_fd)
I think that the bool quiet could be replaced with a flags field
so it can support other flags.
diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index 6aa65d344c72..5c38a11f1a05 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -717,16 +717,12 @@ 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, bool quiet)
+ const int ruleset_fd, const __u64 allowed_access,
+ const char *const path, __u32 flags)
{
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = allowed_access,
};
- __u32 flags = 0;
-
- if (quiet)
- flags |= LANDLOCK_ADD_RULE_QUIET;
path_beneath.parent_fd = open(path, O_PATH | O_CLOEXEC);
ASSERT_LE(0, path_beneath.parent_fd)
And then update the tests to account for the changed
function signature.
I think the bool quiet in the landlock-sandboxer methods
populate_ruleset_fs and populate_ruleset_net (in
samples/landlock/sandboxer.c) should be updated as well,
replacing the bool quiet with a general flags field.
I have change this in my patch but it might make more sense in
your patch since this is the first patch to add flags and would
make it easier if anyone else decides to add flags rebased on
your patch.
Great work and thank you for your help.
Kind Regards,
Justin Suess
^ permalink raw reply related
* Re: [PATCH v5 07/10] selftests/landlock: add tests for quiet flag with fs rules
From: Tingmao Wang @ 2025-11-25 0:57 UTC (permalink / raw)
To: Justin Suess, Mickaël Salaün
Cc: gnoack, jack, xandfury, linux-security-module
In-Reply-To: <20251124143639.3321365-1-utilityemal77@gmail.com>
On 11/24/25 14:36, Justin Suess wrote:
> [...]
> Small suggestion on the tests and samples. I saw you
> added a bool quiet to some methods for the quiet flag.
>
>> diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
>> index 943b6e2ac53d..6aa65d344c72 100644
>> --- a/tools/testing/selftests/landlock/fs_test.c
>> +++ b/tools/testing/selftests/landlock/fs_test.c
>> @@ -718,11 +718,15 @@ TEST_F_FORK(layout1, rule_with_unhandled_access)
>>
>> static void add_path_beneath(struct __test_metadata *const _metadata,
>> const int ruleset_fd, const __u64 allowed_access,
>> - const char *const path)
>> + const char *const path, bool quiet)
>> {
>> struct landlock_path_beneath_attr path_beneath = {
>> .allowed_access = allowed_access,
>> };
>> + __u32 flags = 0;
>> +
>> + if (quiet)
>> + flags |= LANDLOCK_ADD_RULE_QUIET;
>>
>> path_beneath.parent_fd = open(path, O_PATH | O_CLOEXEC);
>> ASSERT_LE(0, path_beneath.parent_fd)
>
>
> I think that the bool quiet could be replaced with a flags field
> so it can support other flags.
>
> diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
> index 6aa65d344c72..5c38a11f1a05 100644
> --- a/tools/testing/selftests/landlock/fs_test.c
> +++ b/tools/testing/selftests/landlock/fs_test.c
> @@ -717,16 +717,12 @@ 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, bool quiet)
> + const int ruleset_fd, const __u64 allowed_access,
> + const char *const path, __u32 flags)
> {
> struct landlock_path_beneath_attr path_beneath = {
> .allowed_access = allowed_access,
> };
> - __u32 flags = 0;
> -
> - if (quiet)
> - flags |= LANDLOCK_ADD_RULE_QUIET;
>
> path_beneath.parent_fd = open(path, O_PATH | O_CLOEXEC);
> ASSERT_LE(0, path_beneath.parent_fd)
>
> And then update the tests to account for the changed
> function signature.
>
> I think the bool quiet in the landlock-sandboxer methods
> populate_ruleset_fs and populate_ruleset_net (in
> samples/landlock/sandboxer.c) should be updated as well,
> replacing the bool quiet with a general flags field.
Good point - I think both suggestions makes sense for future-proofing.
Here are the proper changes, which I will apply to v6. For your
convenience, the new set of commits are available at
https://github.com/micromaomao/linux-dev/pull/13/commits
Mickaël - let me know if you have any other feedback on this series, and I
will send v6 afterwards.
squash! selftests/landlock: add tests for quiet flag with fs rules
diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index 6aa65d344c72..c29ee72b2cc1 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -718,15 +718,11 @@ 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, bool quiet)
+ const char *const path, __u32 flags)
{
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = allowed_access,
};
- __u32 flags = 0;
-
- if (quiet)
- flags |= LANDLOCK_ADD_RULE_QUIET;
path_beneath.parent_fd = open(path, O_PATH | O_CLOEXEC);
ASSERT_LE(0, path_beneath.parent_fd)
@@ -790,7 +786,7 @@ static int create_ruleset(struct __test_metadata *const _metadata,
continue;
add_path_beneath(_metadata, ruleset_fd, rules[i].access,
- rules[i].path, false);
+ rules[i].path, 0);
}
return ruleset_fd;
}
@@ -1368,7 +1364,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, false);
+ dir_s1d2, 0);
/*
* According to ruleset_fd, dir_s1d2 should now have the
* LANDLOCK_ACCESS_FS_READ_FILE and LANDLOCK_ACCESS_FS_WRITE_FILE
@@ -1400,7 +1396,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, false);
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RW, dir_s1d1, 0);
enforce_ruleset(_metadata, ruleset_fd);
/* Same tests and results as above. */
@@ -1423,7 +1419,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, false);
+ dir_s1d3, 0);
enforce_ruleset(_metadata, ruleset_fd);
ASSERT_EQ(0, close(ruleset_fd));
@@ -1476,7 +1472,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, false);
+ dir_s1d2, 0);
enforce_ruleset(_metadata, ruleset_fd);
ASSERT_EQ(0, close(ruleset_fd));
@@ -7647,7 +7643,7 @@ static int apply_a_layer(struct __test_metadata *const _metadata,
continue;
add_path_beneath(_metadata, rs_fd, r->access, r->path,
- r->quiet);
+ r->quiet ? LANDLOCK_ADD_RULE_QUIET : 0);
}
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
squash! samples/landlock: Add quiet flag support to sandboxer
diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
index 2d8e3e94b77b..07dc0013ff19 100644
--- a/samples/landlock/sandboxer.c
+++ b/samples/landlock/sandboxer.c
@@ -121,7 +121,7 @@ static int parse_path(char *env_path, const char ***const path_list)
/* clang-format on */
static int populate_ruleset_fs(const char *const env_var, const int ruleset_fd,
- const __u64 allowed_access, bool quiet)
+ const __u64 allowed_access, __u32 flags)
{
int num_paths, i, ret = 1;
char *env_path_name;
@@ -171,8 +171,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,
- quiet ? LANDLOCK_ADD_RULE_QUIET : 0)) {
+ &path_beneath, flags)) {
fprintf(stderr,
"Failed to update the ruleset with \"%s\": %s\n",
path_list[i], strerror(errno));
@@ -190,7 +189,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, bool quiet)
+ const __u64 allowed_access, __u32 flags)
{
int ret = 1;
char *env_port_name, *env_port_name_next, *strport;
@@ -218,8 +217,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,
- quiet ? LANDLOCK_ADD_RULE_QUIET : 0)) {
+ &net_port, flags)) {
fprintf(stderr,
"Failed to update the ruleset with port \"%llu\": %s\n",
net_port.port, strerror(errno));
@@ -595,35 +593,31 @@ 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,
- false)) {
+ 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,
- false)) {
+ 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,
- true)) {
+ LANDLOCK_ADD_RULE_QUIET))
goto err_close_ruleset;
- }
}
if (populate_ruleset_net(ENV_TCP_BIND_NAME, ruleset_fd,
- LANDLOCK_ACCESS_NET_BIND_TCP, false)) {
+ 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, false)) {
+ LANDLOCK_ACCESS_NET_CONNECT_TCP, 0)) {
goto err_close_ruleset;
}
/* Don't require this env to be present. */
if (quiet_supported && getenv(ENV_NET_QUIET_NAME)) {
if (populate_ruleset_net(ENV_NET_QUIET_NAME, ruleset_fd, 0,
- true)) {
+ LANDLOCK_ADD_RULE_QUIET)) {
goto err_close_ruleset;
}
}
^ permalink raw reply related
* Re: [PATCH v2] lockdown: Only log restrictions once
From: Nicolas Bouchinet @ 2025-11-25 10:00 UTC (permalink / raw)
To: Xiujianfeng
Cc: Daniel Tang, Xiu Jianfeng, Paul Moore, linux-security-module,
linux-kernel, Nathan Lynch, Matthew Garrett, Kees Cook,
David Howells, James Morris
In-Reply-To: <2f4a1af8-adc6-4cbc-813f-4cc8e9bc75ae@huaweicloud.com>
Hi,
> Currently lockdown does not support the audit function, so I believe the
> logs here serve a purpose similar to auditing. Based on this, I think
> this change will meaningfully degrade the quality of the logs, making it
> hard for users to find out what happens when lockdown is active,
> especially after a long time running.
I agree with Xiu.
I'm not sure to understand how this is a kernel issue. I mean beside
that we do not support hibernation in Lockdown for now.
Can't you just disable hibernation with systemd-logind using someting like
'AllowHibernation=no' ?
Best regards,
Nicolas
^ permalink raw reply
* Re: [RFC][PATCH] exec: Move cred computation under exec_update_lock
From: Roberto Sassu @ 2025-11-25 11:55 UTC (permalink / raw)
To: Eric W. Biederman, Bernd Edlinger
Cc: Alexander Viro, Alexey Dobriyan, Oleg Nesterov, Kees Cook,
Andy Lutomirski, Will Drewry, Christian Brauner, Andrew Morton,
Michal Hocko, Serge Hallyn, James Morris, Randy Dunlap,
Suren Baghdasaryan, Yafang Shao, Helge Deller, Adrian Reber,
Thomas Gleixner, Jens Axboe, Alexei Starovoitov,
linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org,
linux-kselftest, linux-mm, linux-security-module, tiozhang,
Luis Chamberlain, Paulo Alcantara (SUSE), Sergey Senozhatsky,
Frederic Weisbecker, YueHaibing, Paul Moore, Aleksa Sarai,
Stefan Roesch, Chao Yu, xu xin, Jeff Layton, Jan Kara,
David Hildenbrand, Dave Chinner, Shuah Khan, Elena Reshetova,
David Windsor, Mateusz Guzik, Ard Biesheuvel,
Joel Fernandes (Google), Matthew Wilcox (Oracle),
Hans Liljestrand, Penglei Jiang, Lorenzo Stoakes, Adrian Ratiu,
Ingo Molnar, Peter Zijlstra (Intel), Cyrill Gorcunov,
Eric Dumazet, zohar, linux-integrity
In-Reply-To: <87h5uoxw06.fsf_-_@email.froward.int.ebiederm.org>
On Thu, 2025-11-20 at 14:57 -0600, Eric W. Biederman wrote:
> Instead of computing the new cred before we pass the point of no
> return compute the new cred just before we use it.
>
> This allows the removal of fs_struct->in_exec and cred_guard_mutex.
>
> I am not certain why we wanted to compute the cred for the new
> executable so early. Perhaps I missed something but I did not see any
> common errors being signaled. So I don't think we loose anything by
> computing the new cred later.
>
> We gain a lot.
>
> We stop holding the cred_guard_mutex over places where the code sleeps
> and waits for userspace. These places include the waiting for the
> tracer in PTRACE_EVENT_EXIT, "put_user(0, tsk->clear_child_tid)" in
> mm_release, and "get_user(futex_offset, ...") in exit_robust_mutex.
>
> We can remove fs_struct->in_exec. The case where it was used simply
> never comes up, when we compute the cred after de_thread completes.
>
> We remove the possibility of a hang between a tracer calling
> PTRACE_ATTACH/PTRACE_SIEZE and the kernel waiting for the tracer
> in PTRACE_EVENT_EXIT.
>
> ---
> Oleg, Kees, Bernd, Can you see anything I am missing?
+ Mimi, linux-integrity (would be nice if we are in CC when linux-
security-module is in CC).
Apologies for not answering earlier, it seems I don't receive the
emails from the linux-security-module mailing list (thanks Serge for
letting me know!).
I tested your patch but there are a few warnings like this:
[ 2.702374] =====================================
[ 2.702854] WARNING: bad unlock balance detected!
[ 2.703350] 6.18.0-rc6+ #409 Not tainted
[ 2.703755] -------------------------------------
[ 2.704241] init/1 is trying to release lock (init_fs.seq) at:
[ 2.704829] [<ffffffff81836100>] begin_new_exec+0xfe0/0x1710
[ 2.705421] but there are no more locks to release!
[ 2.705931]
[ 2.705931] other info that might help us debug this:
[ 2.706610] 1 lock held by init/1:
[ 2.706958] #0: ffff88810083e538 (&sig->exec_update_lock){+.+.}-{4:4}, at: begin_new_exec+0x769/0x1710
and then the system hangs.
I see two main effects of this patch. First, the bprm_check_security
hook implementations will not see bprm->cred populated. That was a
problem before we made this patch:
https://patchew.org/linux/20251008113503.2433343-1-roberto.sassu@huaweicloud.com/
to work around the problem of not calculating the final DAC credentials
early enough (well, we actually had to change our CREDS_CHECK hook
behavior).
The second, I could not check. If I remember well, unlike the
capability LSM, SELinux/Apparmor/SMACK calculate the final credentials
based on the first file being executed (thus the script, not the
interpreter). Is this patch keeping the same behavior despite preparing
the credentials when the final binary is found?
Thanks
Roberto
> The code compiles but I haven't test it yet.
>
> I thought I was going to move commit_creds before de_thread, but that
> would have taken commit_cred out of exec_update_lock (which introduces
> races).
>
> However I can't see any drawbacks of going the other direction.
>
>
> fs/exec.c | 88 ++++++++++++++----------------------
> fs/fs_struct.c | 1 -
> fs/proc/base.c | 4 +-
> include/linux/fs_struct.h | 1 -
> include/linux/sched/signal.h | 6 ---
> init/init_task.c | 1 -
> kernel/cred.c | 2 +-
> kernel/fork.c | 8 +---
> kernel/ptrace.c | 4 +-
> kernel/seccomp.c | 12 ++---
> 10 files changed, 45 insertions(+), 82 deletions(-)
>
> diff --git a/fs/exec.c b/fs/exec.c
> index 4298e7e08d5d..5ae96584dab0 100644
> --- a/fs/exec.c
> +++ b/fs/exec.c
> @@ -1090,6 +1090,9 @@ void __set_task_comm(struct task_struct *tsk, const char *buf, bool exec)
> perf_event_comm(tsk, exec);
> }
>
> +static int prepare_bprm_creds(struct linux_binprm *bprm);
> +static void check_unsafe_exec(struct linux_binprm *bprm);
> +
> /*
> * Calling this is the point of no return. None of the failures will be
> * seen by userspace since either the process is already taking a fatal
> @@ -1101,10 +1104,6 @@ int begin_new_exec(struct linux_binprm * bprm)
> struct task_struct *me = current;
> int retval;
>
> - /* Once we are committed compute the creds */
> - retval = bprm_creds_from_file(bprm);
> - if (retval)
> - return retval;
>
> /*
> * This tracepoint marks the point before flushing the old exec where
> @@ -1123,8 +1122,6 @@ int begin_new_exec(struct linux_binprm * bprm)
> retval = de_thread(me);
> if (retval)
> goto out;
> - /* see the comment in check_unsafe_exec() */
> - current->fs->in_exec = 0;
> /*
> * Cancel any io_uring activity across execve
> */
> @@ -1251,6 +1248,25 @@ int begin_new_exec(struct linux_binprm * bprm)
> WRITE_ONCE(me->self_exec_id, me->self_exec_id + 1);
> flush_signal_handlers(me, 0);
>
> + retval = prepare_bprm_creds(bprm);
> + if (retval)
> + goto out_unlock;
> +
> + /*
> + * Check for unsafe execution states before exec_binprm(), which
> + * will call back into begin_new_exec(), into bprm_creds_from_file(),
> + * where setuid-ness is evaluated.
> + */
> + check_unsafe_exec(bprm);
> +
> + /* Set the unchanging part of bprm->cred */
> + retval = security_bprm_creds_for_exec(bprm);
> +
> + /* Once we are committed compute the creds */
> + retval = bprm_creds_from_file(bprm);
> + if (retval)
> + goto out_unlock;
> +
> retval = set_cred_ucounts(bprm->cred);
> if (retval < 0)
> goto out_unlock;
> @@ -1272,9 +1288,9 @@ int begin_new_exec(struct linux_binprm * bprm)
> if (get_dumpable(me->mm) != SUID_DUMP_USER)
> perf_event_exit_task(me);
> /*
> - * cred_guard_mutex must be held at least to this point to prevent
> + * exec_update_lock must be held at least to this point to prevent
> * ptrace_attach() from altering our determination of the task's
> - * credentials; any time after this it may be unlocked.
> + * credentials.
> */
> security_bprm_committed_creds(bprm);
>
> @@ -1291,8 +1307,6 @@ int begin_new_exec(struct linux_binprm * bprm)
>
> out_unlock:
> up_write(&me->signal->exec_update_lock);
> - if (!bprm->cred)
> - mutex_unlock(&me->signal->cred_guard_mutex);
>
> out:
> return retval;
> @@ -1336,7 +1350,6 @@ void setup_new_exec(struct linux_binprm * bprm)
> */
> me->mm->task_size = TASK_SIZE;
> up_write(&me->signal->exec_update_lock);
> - mutex_unlock(&me->signal->cred_guard_mutex);
> }
> EXPORT_SYMBOL(setup_new_exec);
>
> @@ -1351,21 +1364,15 @@ void finalize_exec(struct linux_binprm *bprm)
> EXPORT_SYMBOL(finalize_exec);
>
> /*
> - * Prepare credentials and lock ->cred_guard_mutex.
> - * setup_new_exec() commits the new creds and drops the lock.
> - * Or, if exec fails before, free_bprm() should release ->cred
> - * and unlock.
> + * Prepare credentials. begin_new_exec() commits the new creds.
> + * Or, if exec fails before, free_bprm() should release ->cred.
> */
> static int prepare_bprm_creds(struct linux_binprm *bprm)
> {
> - if (mutex_lock_interruptible(¤t->signal->cred_guard_mutex))
> - return -ERESTARTNOINTR;
> -
> bprm->cred = prepare_exec_creds();
> if (likely(bprm->cred))
> return 0;
>
> - mutex_unlock(¤t->signal->cred_guard_mutex);
> return -ENOMEM;
> }
>
> @@ -1386,9 +1393,7 @@ static void free_bprm(struct linux_binprm *bprm)
> }
> free_arg_pages(bprm);
> if (bprm->cred) {
> - /* in case exec fails before de_thread() succeeds */
> - current->fs->in_exec = 0;
> - mutex_unlock(¤t->signal->cred_guard_mutex);
> + /* in case exec fails before commit_creds succeeds */
> abort_creds(bprm->cred);
> }
> do_close_execat(bprm->file);
> @@ -1486,13 +1491,12 @@ EXPORT_SYMBOL(bprm_change_interp);
>
> /*
> * determine how safe it is to execute the proposed program
> - * - the caller must hold ->cred_guard_mutex to protect against
> + * - the caller must hold ->exec_update_lock to protect against
> * PTRACE_ATTACH or seccomp thread-sync
> */
> static void check_unsafe_exec(struct linux_binprm *bprm)
> {
> - struct task_struct *p = current, *t;
> - unsigned n_fs;
> + struct task_struct *p = current;
>
> if (p->ptrace)
> bprm->unsafe |= LSM_UNSAFE_PTRACE;
> @@ -1509,25 +1513,9 @@ static void check_unsafe_exec(struct linux_binprm *bprm)
> * suid exec because the differently privileged task
> * will be able to manipulate the current directory, etc.
> * It would be nice to force an unshare instead...
> - *
> - * Otherwise we set fs->in_exec = 1 to deny clone(CLONE_FS)
> - * from another sub-thread until de_thread() succeeds, this
> - * state is protected by cred_guard_mutex we hold.
> */
> - n_fs = 1;
> - read_seqlock_excl(&p->fs->seq);
> - rcu_read_lock();
> - for_other_threads(p, t) {
> - if (t->fs == p->fs)
> - n_fs++;
> - }
> - rcu_read_unlock();
> -
> - /* "users" and "in_exec" locked for copy_fs() */
> - if (p->fs->users > n_fs)
> + if (p->fs->users > 1)
> bprm->unsafe |= LSM_UNSAFE_SHARE;
> - else
> - p->fs->in_exec = 1;
> read_sequnlock_excl(&p->fs->seq);
> }
>
> @@ -1731,25 +1719,15 @@ static int bprm_execve(struct linux_binprm *bprm)
> {
> int retval;
>
> - retval = prepare_bprm_creds(bprm);
> - if (retval)
> - return retval;
> + if (bprm->is_check)
> + return 0;
>
> - /*
> - * Check for unsafe execution states before exec_binprm(), which
> - * will call back into begin_new_exec(), into bprm_creds_from_file(),
> - * where setuid-ness is evaluated.
> - */
> - check_unsafe_exec(bprm);
> current->in_execve = 1;
> sched_mm_cid_before_execve(current);
>
> sched_exec();
>
> - /* Set the unchanging part of bprm->cred */
> - retval = security_bprm_creds_for_exec(bprm);
> - if (retval || bprm->is_check)
> - goto out;
> +
>
> retval = exec_binprm(bprm);
> if (retval < 0)
> diff --git a/fs/fs_struct.c b/fs/fs_struct.c
> index 28be762ac1c6..945bc0916f65 100644
> --- a/fs/fs_struct.c
> +++ b/fs/fs_struct.c
> @@ -109,7 +109,6 @@ struct fs_struct *copy_fs_struct(struct fs_struct *old)
> /* We don't need to lock fs - think why ;-) */
> if (fs) {
> fs->users = 1;
> - fs->in_exec = 0;
> seqlock_init(&fs->seq);
> fs->umask = old->umask;
>
> diff --git a/fs/proc/base.c b/fs/proc/base.c
> index 6299878e3d97..7041fb4d1689 100644
> --- a/fs/proc/base.c
> +++ b/fs/proc/base.c
> @@ -2834,14 +2834,14 @@ static ssize_t proc_pid_attr_write(struct file * file, const char __user * buf,
> }
>
> /* Guard against adverse ptrace interaction */
> - rv = mutex_lock_interruptible(¤t->signal->cred_guard_mutex);
> + rv = down_write_killable(¤t->signal->exec_update_lock);
> if (rv < 0)
> goto out_free;
>
> rv = security_setprocattr(PROC_I(inode)->op.lsmid,
> file->f_path.dentry->d_name.name, page,
> count);
> - mutex_unlock(¤t->signal->cred_guard_mutex);
> + up_write(¤t->signal->exec_update_lock);
> out_free:
> kfree(page);
> out:
> diff --git a/include/linux/fs_struct.h b/include/linux/fs_struct.h
> index baf200ab5c77..29d0f7d57743 100644
> --- a/include/linux/fs_struct.h
> +++ b/include/linux/fs_struct.h
> @@ -10,7 +10,6 @@ struct fs_struct {
> int users;
> seqlock_t seq;
> int umask;
> - int in_exec;
> struct path root, pwd;
> } __randomize_layout;
>
> diff --git a/include/linux/sched/signal.h b/include/linux/sched/signal.h
> index 7d6449982822..7e9259c8fb2b 100644
> --- a/include/linux/sched/signal.h
> +++ b/include/linux/sched/signal.h
> @@ -241,12 +241,6 @@ struct signal_struct {
> struct mm_struct *oom_mm; /* recorded mm when the thread group got
> * killed by the oom killer */
>
> - struct mutex cred_guard_mutex; /* guard against foreign influences on
> - * credential calculations
> - * (notably. ptrace)
> - * Deprecated do not use in new code.
> - * Use exec_update_lock instead.
> - */
> struct rw_semaphore exec_update_lock; /* Held while task_struct is
> * being updated during exec,
> * and may have inconsistent
> diff --git a/init/init_task.c b/init/init_task.c
> index a55e2189206f..4813bffe217e 100644
> --- a/init/init_task.c
> +++ b/init/init_task.c
> @@ -30,7 +30,6 @@ static struct signal_struct init_signals = {
> #ifdef CONFIG_CGROUPS
> .cgroup_threadgroup_rwsem = __RWSEM_INITIALIZER(init_signals.cgroup_threadgroup_rwsem),
> #endif
> - .cred_guard_mutex = __MUTEX_INITIALIZER(init_signals.cred_guard_mutex),
> .exec_update_lock = __RWSEM_INITIALIZER(init_signals.exec_update_lock),
> #ifdef CONFIG_POSIX_TIMERS
> .posix_timers = HLIST_HEAD_INIT,
> diff --git a/kernel/cred.c b/kernel/cred.c
> index dbf6b687dc5c..80e376ce005f 100644
> --- a/kernel/cred.c
> +++ b/kernel/cred.c
> @@ -252,7 +252,7 @@ EXPORT_SYMBOL(prepare_creds);
>
> /*
> * Prepare credentials for current to perform an execve()
> - * - The caller must hold ->cred_guard_mutex
> + * - The caller must hold ->exec_update_lock
> */
> struct cred *prepare_exec_creds(void)
> {
> diff --git a/kernel/fork.c b/kernel/fork.c
> index 3da0f08615a9..996c649b9a4c 100644
> --- a/kernel/fork.c
> +++ b/kernel/fork.c
> @@ -1555,11 +1555,6 @@ static int copy_fs(u64 clone_flags, struct task_struct *tsk)
> if (clone_flags & CLONE_FS) {
> /* tsk->fs is already what we want */
> read_seqlock_excl(&fs->seq);
> - /* "users" and "in_exec" locked for check_unsafe_exec() */
> - if (fs->in_exec) {
> - read_sequnlock_excl(&fs->seq);
> - return -EAGAIN;
> - }
> fs->users++;
> read_sequnlock_excl(&fs->seq);
> return 0;
> @@ -1699,7 +1694,6 @@ static int copy_signal(u64 clone_flags, struct task_struct *tsk)
> sig->oom_score_adj = current->signal->oom_score_adj;
> sig->oom_score_adj_min = current->signal->oom_score_adj_min;
>
> - mutex_init(&sig->cred_guard_mutex);
> init_rwsem(&sig->exec_update_lock);
>
> return 0;
> @@ -1710,7 +1704,7 @@ static void copy_seccomp(struct task_struct *p)
> #ifdef CONFIG_SECCOMP
> /*
> * Must be called with sighand->lock held, which is common to
> - * all threads in the group. Holding cred_guard_mutex is not
> + * all threads in the group. Holding exec_update_lock is not
> * needed because this new task is not yet running and cannot
> * be racing exec.
> */
> diff --git a/kernel/ptrace.c b/kernel/ptrace.c
> index 75a84efad40f..8140d4bfc279 100644
> --- a/kernel/ptrace.c
> +++ b/kernel/ptrace.c
> @@ -444,8 +444,8 @@ static int ptrace_attach(struct task_struct *task, long request,
> * SUID, SGID and LSM creds get determined differently
> * under ptrace.
> */
> - scoped_cond_guard (mutex_intr, return -ERESTARTNOINTR,
> - &task->signal->cred_guard_mutex) {
> + scoped_cond_guard (rwsem_read_intr, return -ERESTARTNOINTR,
> + &task->signal->exec_update_lock) {
>
> scoped_guard (task_lock, task) {
> retval = __ptrace_may_access(task, PTRACE_MODE_ATTACH_REALCREDS);
> diff --git a/kernel/seccomp.c b/kernel/seccomp.c
> index 25f62867a16d..87de8d47d876 100644
> --- a/kernel/seccomp.c
> +++ b/kernel/seccomp.c
> @@ -479,7 +479,7 @@ static int is_ancestor(struct seccomp_filter *parent,
> /**
> * seccomp_can_sync_threads: checks if all threads can be synchronized
> *
> - * Expects sighand and cred_guard_mutex locks to be held.
> + * Expects sighand and exec_update_lock locks to be held.
> *
> * Returns 0 on success, -ve on error, or the pid of a thread which was
> * either not in the correct seccomp mode or did not have an ancestral
> @@ -489,7 +489,7 @@ static inline pid_t seccomp_can_sync_threads(void)
> {
> struct task_struct *thread, *caller;
>
> - BUG_ON(!mutex_is_locked(¤t->signal->cred_guard_mutex));
> + BUG_ON(!rwsem_is_locked(¤t->signal->exec_update_lock));
> assert_spin_locked(¤t->sighand->siglock);
>
> /* Validate all threads being eligible for synchronization. */
> @@ -590,7 +590,7 @@ void seccomp_filter_release(struct task_struct *tsk)
> *
> * @flags: SECCOMP_FILTER_FLAG_* flags to set during sync.
> *
> - * Expects sighand and cred_guard_mutex locks to be held, and for
> + * Expects sighand and exec_update_lock locks to be held, and for
> * seccomp_can_sync_threads() to have returned success already
> * without dropping the locks.
> *
> @@ -599,7 +599,7 @@ static inline void seccomp_sync_threads(unsigned long flags)
> {
> struct task_struct *thread, *caller;
>
> - BUG_ON(!mutex_is_locked(¤t->signal->cred_guard_mutex));
> + BUG_ON(!rwsem_is_locked(¤t->signal->exec_update_lock));
> assert_spin_locked(¤t->sighand->siglock);
>
> /*
> @@ -2011,7 +2011,7 @@ static long seccomp_set_mode_filter(unsigned int flags,
> * while another thread is in the middle of calling exec.
> */
> if (flags & SECCOMP_FILTER_FLAG_TSYNC &&
> - mutex_lock_killable(¤t->signal->cred_guard_mutex))
> + down_read_killable(¤t->signal->exec_update_lock))
> goto out_put_fd;
>
> spin_lock_irq(¤t->sighand->siglock);
> @@ -2034,7 +2034,7 @@ static long seccomp_set_mode_filter(unsigned int flags,
> out:
> spin_unlock_irq(¤t->sighand->siglock);
> if (flags & SECCOMP_FILTER_FLAG_TSYNC)
> - mutex_unlock(¤t->signal->cred_guard_mutex);
> + up_read(¤t->signal->exec_update_lock);
> out_put_fd:
> if (flags & SECCOMP_FILTER_FLAG_NEW_LISTENER) {
> if (ret) {
^ permalink raw reply
* Re: [PATCH 2/6] landlock: Implement LANDLOCK_ADD_RULE_NO_INHERIT userspace api
From: Justin Suess @ 2025-11-25 12:06 UTC (permalink / raw)
To: m; +Cc: gnoack, jack, linux-security-module, mic, utilityemal77, xandfury
In-Reply-To: <59aa2857-46d0-4527-990f-03fd6bf13305@maowtm.org>
Good catch.
Probably just gonna add that comment to the add_rule_path_beneath
since LANDLOCK_ADD_RULE_NO_INHERIT doesn't really apply to networking
stuff at all and really doesn't make sense in those rules.
I may even include some code barring the flag from being included in
irrelevant scopes.
Networking, sockets, and signals don't really have an inheritance
behavior.
I personally don't really see how this flag could apply to any
other scopes but if anyone has ideas I'd love to hear them.
If other hierarchical scopes get added then this flag can support those.
Or maybe this flag can have in a different meaning in those contexts.
Thank You,
Justin Suess
^ permalink raw reply
* Re: [PATCH 0/2] apparmor unaligned memory fixes
From: Helge Deller @ 2025-11-25 15:11 UTC (permalink / raw)
To: John Johansen, John Paul Adrian Glaubitz
Cc: Helge Deller, linux-kernel, apparmor, linux-security-module,
linux-parisc
In-Reply-To: <ba3d5651-fa68-4bb5-84aa-35576044e7b0@canonical.com>
* John Johansen <john.johansen@canonical.com>:
> On 11/18/25 04:49, Helge Deller wrote:
> > Hi Adrian,
> >
> > On 11/18/25 12:43, John Paul Adrian Glaubitz wrote:
> > > On Tue, 2025-11-18 at 12:09 +0100, Helge Deller wrote:
> > > > My patch fixed two call sites, but I suspect you see another call site which
> > > > hasn't been fixed yet.
> > > >
> > > > Can you try attached patch? It might indicate the caller of the function and
> > > > maybe prints the struct name/address which isn't aligned.
> > > >
> > > > Helge
> > > >
> > > >
> > > > diff --git a/security/apparmor/match.c b/security/apparmor/match.c
> > > > index c5a91600842a..b477430c07eb 100644
> > > > --- a/security/apparmor/match.c
> > > > +++ b/security/apparmor/match.c
> > > > @@ -313,6 +313,9 @@ struct aa_dfa *aa_dfa_unpack(void *blob, size_t size, int flags)
> > > > if (size < sizeof(struct table_set_header))
> > > > goto fail;
> > > > + if (WARN_ON(((unsigned long)data) & (BITS_PER_LONG/8 - 1)))
> > > > + pr_warn("dfa blob stream %pS not aligned.\n", data);
> > > > +
> > > > if (ntohl(*(__be32 *) data) != YYTH_MAGIC)
> > > > goto fail;
> > >
> > > Here is the relevant output with the patch applied:
> > >
> > > [ 73.840639] ------------[ cut here ]------------
> > > [ 73.901376] WARNING: CPU: 0 PID: 341 at security/apparmor/match.c:316 aa_dfa_unpack+0x6cc/0x720
> > > [ 74.015867] Modules linked in: binfmt_misc evdev flash sg drm drm_panel_orientation_quirks backlight i2c_core configfs nfnetlink autofs4 ext4 crc16 mbcache jbd2 hid_generic usbhid sr_mod hid cdrom
> > > sd_mod ata_generic ohci_pci ehci_pci ehci_hcd ohci_hcd pata_ali libata sym53c8xx scsi_transport_spi tg3 scsi_mod usbcore libphy scsi_common mdio_bus usb_common
> > > [ 74.428977] CPU: 0 UID: 0 PID: 341 Comm: apparmor_parser Not tainted 6.18.0-rc6+ #9 NONE
> > > [ 74.536543] Call Trace:
> > > [ 74.568561] [<0000000000434c24>] dump_stack+0x8/0x18
> > > [ 74.633757] [<0000000000476438>] __warn+0xd8/0x100
> > > [ 74.696664] [<00000000004296d4>] warn_slowpath_fmt+0x34/0x74
> > > [ 74.771006] [<00000000008db28c>] aa_dfa_unpack+0x6cc/0x720
> > > [ 74.843062] [<00000000008e643c>] unpack_pdb+0xbc/0x7e0
> > > [ 74.910545] [<00000000008e7740>] unpack_profile+0xbe0/0x1300
> > > [ 74.984888] [<00000000008e82e0>] aa_unpack+0xe0/0x6a0
> > > [ 75.051226] [<00000000008e3ec4>] aa_replace_profiles+0x64/0x1160
> > > [ 75.130144] [<00000000008d4d90>] policy_update+0xf0/0x280
> > > [ 75.201057] [<00000000008d4fc8>] profile_replace+0xa8/0x100
> > > [ 75.274258] [<0000000000766bd0>] vfs_write+0x90/0x420
> > > [ 75.340594] [<00000000007670cc>] ksys_write+0x4c/0xe0
> > > [ 75.406932] [<0000000000767174>] sys_write+0x14/0x40
> > > [ 75.472126] [<0000000000406174>] linux_sparc_syscall+0x34/0x44
> > > [ 75.548802] ---[ end trace 0000000000000000 ]---
> > > [ 75.609503] dfa blob stream 0xfff0000008926b96 not aligned.
> > > [ 75.682695] Kernel unaligned access at TPC[8db2a8] aa_dfa_unpack+0x6e8/0x720
> >
> > The non-8-byte-aligned address (0xfff0000008926b96) is coming from userspace
> > (via the write syscall).
> > Some apparmor userspace tool writes into the apparmor ".replace" virtual file with
> > a source address which is not correctly aligned.
>
> the userpace buffer passed to write(2) has to be aligned? Its certainly nice if it
> is but the userspace tooling hasn't been treating it as aligned. With that said,
> the dfa should be padded to be aligned. So this tripping in the dfa is a bug,
> and there really should be some validation to catch it.
>
> > You should be able to debug/find the problematic code with strace from userspace.
> > Maybe someone with apparmor knowledge here on the list has an idea?
> >
> This is likely an unaligned 2nd profile, being split out and loaded separately
> from the rest of the container. Basically the loader for some reason (there
> are a few different possible reasons) is poking into the container format and
> pulling out the profile at some offset, this gets loaded to the kernel but
> it would seem that its causing an issue with the dfa alignment within the container,
> which should be aligned to the original container.
Regarding this:
> Kernel side, we are going to need to add some extra verification checks, it should
> be catching this, as unaligned as part of the unpack. Userspace side, we will have
> to verify my guess and fix the loader.
I wonder if loading those tables are really time critical?
If not, maybe just making the kernel aware that the tables might be unaligned
can help, e.g. with the following (untested) patch.
Adrian, maybe you want to test?
------------------------
[PATCH] Allow apparmor to handle unaligned dfa tables
The dfa tables can originate from kernel or userspace and 8-byte alignment
isn't always guaranteed and as such may trigger unaligned memory accesses
on various architectures.
Work around it by using the get_unaligned_xx() helpers.
Signed-off-by: Helge Deller <deller@gmx.de>
diff --git a/security/apparmor/match.c b/security/apparmor/match.c
index c5a91600842a..26e82ba879d4 100644
--- a/security/apparmor/match.c
+++ b/security/apparmor/match.c
@@ -15,6 +15,7 @@
#include <linux/vmalloc.h>
#include <linux/err.h>
#include <linux/kref.h>
+#include <linux/unaligned.h>
#include "include/lib.h"
#include "include/match.h"
@@ -42,11 +43,11 @@ static struct table_header *unpack_table(char *blob, size_t bsize)
/* loaded td_id's start at 1, subtract 1 now to avoid doing
* it every time we use td_id as an index
*/
- th.td_id = be16_to_cpu(*(__be16 *) (blob)) - 1;
+ th.td_id = get_unaligned_be16(blob) - 1;
if (th.td_id > YYTD_ID_MAX)
goto out;
- th.td_flags = be16_to_cpu(*(__be16 *) (blob + 2));
- th.td_lolen = be32_to_cpu(*(__be32 *) (blob + 8));
+ th.td_flags = get_unaligned_be16(blob + 2);
+ th.td_lolen = get_unaligned_be32(blob + 8);
blob += sizeof(struct table_header);
if (!(th.td_flags == YYTD_DATA16 || th.td_flags == YYTD_DATA32 ||
@@ -313,14 +314,14 @@ struct aa_dfa *aa_dfa_unpack(void *blob, size_t size, int flags)
if (size < sizeof(struct table_set_header))
goto fail;
- if (ntohl(*(__be32 *) data) != YYTH_MAGIC)
+ if (get_unaligned_be32(data) != YYTH_MAGIC)
goto fail;
- hsize = ntohl(*(__be32 *) (data + 4));
+ hsize = get_unaligned_be32(data + 4);
if (size < hsize)
goto fail;
- dfa->flags = ntohs(*(__be16 *) (data + 12));
+ dfa->flags = get_unaligned_be16(data + 12);
if (dfa->flags & ~(YYTH_FLAGS))
goto fail;
@@ -329,7 +330,7 @@ struct aa_dfa *aa_dfa_unpack(void *blob, size_t size, int flags)
* if (dfa->flags & YYTH_FLAGS_OOB_TRANS) {
* if (hsize < 16 + 4)
* goto fail;
- * dfa->max_oob = ntol(*(__be32 *) (data + 16));
+ * dfa->max_oob = get_unaligned_be32(data + 16);
* if (dfa->max <= MAX_OOB_SUPPORTED) {
* pr_err("AppArmor DFA OOB greater than supported\n");
* goto fail;
^ permalink raw reply related
* Re: [PATCH v2] security: Rename functions and add namespace mapping tests
From: Serge E. Hallyn @ 2025-11-25 15:15 UTC (permalink / raw)
To: Ryan Foster; +Cc: serge, paul, linux-security-module, linux-kernel
In-Reply-To: <20251121174826.190381-1-foster.ryan.r@gmail.com>
On Fri, Nov 21, 2025 at 09:48:26AM -0800, Ryan Foster wrote:
> Rename rootid_owns_currentns() to uid_owns_currentns() and
> rootid_owns_userns() to uid_owns_ns() for clarity, as the function checks
> any UID, not just root. Update all call sites accordingly.
>
> Add tests that create actual user namespaces with different UID mappings
> to verify namespace traversal logic. The tests create namespaces where
> uid 0 maps to different kuids (e.g., kuid 1000, 2000) and verify that
> uid_owns_ns() correctly identifies ownership based on the namespace
> hierarchy traversal.
>
> This addresses feedback to use clearer function naming and test actual
> namespace functionality with real user namespace creation and mappings,
> rather than just basic input validation.
Hi Ryan,
did you see https://lore.kernel.org/all/aR0JrOvDxDKZPELd@mail.hallyn.com ?
That is now in linux-next, and should be merged into 6.19 when that window
opens. So please base your patch on that (so you can drop your uid_owns_ns()
renames).
I haven't looked closely at the tests, but at a cursory glance this is what
I had in mind, thanks! I'll look more closely when you send next version.
> ---
> security/commoncap.c | 26 ++--
> security/commoncap_test.c | 286 ++++++++++++++++++++++++++++++++------
> 2 files changed, 254 insertions(+), 58 deletions(-)
>
> diff --git a/security/commoncap.c b/security/commoncap.c
> index 15d8147a34c4..cca291df9551 100644
> --- a/security/commoncap.c
> +++ b/security/commoncap.c
> @@ -359,16 +359,16 @@ int cap_inode_killpriv(struct mnt_idmap *idmap, struct dentry *dentry)
> }
>
> #ifdef CONFIG_SECURITY_COMMONCAP_KUNIT_TEST
> -bool rootid_owns_userns(struct user_namespace *ns, kuid_t kroot);
> -bool rootid_owns_userns(struct user_namespace *ns, kuid_t kroot)
> +bool uid_owns_ns(struct user_namespace *ns, kuid_t kuid);
> +bool uid_owns_ns(struct user_namespace *ns, kuid_t kuid)
> #else
> -static bool rootid_owns_userns(struct user_namespace *ns, kuid_t kroot)
> +static bool uid_owns_ns(struct user_namespace *ns, kuid_t kuid)
> #endif
> {
> struct user_namespace *iter;
>
> for (iter = ns;; iter = iter->parent) {
> - if (from_kuid(iter, kroot) == 0)
> + if (from_kuid(iter, kuid) == 0)
> return true;
> if (iter == &init_user_ns)
> break;
> @@ -378,19 +378,19 @@ static bool rootid_owns_userns(struct user_namespace *ns, kuid_t kroot)
> }
>
> #ifdef CONFIG_SECURITY_COMMONCAP_KUNIT_TEST
> -bool rootid_owns_currentns(vfsuid_t rootvfsuid);
> -bool rootid_owns_currentns(vfsuid_t rootvfsuid)
> +bool uid_owns_currentns(vfsuid_t vfsuid);
> +bool uid_owns_currentns(vfsuid_t vfsuid)
> #else
> -static bool rootid_owns_currentns(vfsuid_t rootvfsuid)
> +static bool uid_owns_currentns(vfsuid_t vfsuid)
> #endif
> {
> - kuid_t kroot;
> + kuid_t kuid;
>
> - if (!vfsuid_valid(rootvfsuid))
> + if (!vfsuid_valid(vfsuid))
> return false;
>
> - kroot = vfsuid_into_kuid(rootvfsuid);
> - return rootid_owns_userns(current_user_ns(), kroot);
> + kuid = vfsuid_into_kuid(vfsuid);
> + return uid_owns_ns(current_user_ns(), kuid);
> }
>
> static __u32 sansflags(__u32 m)
> @@ -497,7 +497,7 @@ int cap_inode_getsecurity(struct mnt_idmap *idmap,
> goto out_free;
> }
>
> - if (!rootid_owns_currentns(vfsroot)) {
> + if (!uid_owns_currentns(vfsroot)) {
> size = -EOVERFLOW;
> goto out_free;
> }
> @@ -738,7 +738,7 @@ int get_vfs_caps_from_disk(struct mnt_idmap *idmap,
> /* Limit the caps to the mounter of the filesystem
> * or the more limited uid specified in the xattr.
> */
> - if (!rootid_owns_currentns(rootvfsuid))
> + if (!uid_owns_currentns(rootvfsuid))
> return -ENODATA;
>
> cpu_caps->permitted.val = le32_to_cpu(caps->data[0].permitted);
> diff --git a/security/commoncap_test.c b/security/commoncap_test.c
> index 962aa899455d..7f066dc0df5d 100644
> --- a/security/commoncap_test.c
> +++ b/security/commoncap_test.c
> @@ -10,6 +10,8 @@
> #include <linux/user_namespace.h>
> #include <linux/uidgid.h>
> #include <linux/module.h>
> +#include <linux/slab.h>
> +#include <linux/refcount.h>
>
> /* Forward declare types and functions we need from mnt_idmapping.h
> * We avoid including the full header because it contains inline functions
> @@ -50,38 +52,38 @@ static inline kuid_t vfsuid_into_kuid(vfsuid_t vfsuid)
> #ifdef CONFIG_SECURITY_COMMONCAP_KUNIT_TEST
>
> /* Forward declarations - functions are exported when KUNIT_TEST is enabled */
> -extern bool rootid_owns_userns(struct user_namespace *ns, kuid_t kroot);
> -extern bool rootid_owns_currentns(vfsuid_t rootvfsuid);
> +extern bool uid_owns_ns(struct user_namespace *ns, kuid_t kuid);
> +extern bool uid_owns_currentns(vfsuid_t vfsuid);
>
> /**
> - * test_rootid_owns_currentns_init_ns - Test rootid_owns_currentns with init ns
> + * test_uid_owns_currentns_init_ns - Test uid_owns_currentns with init ns
> *
> - * Verifies that a root ID in the init namespace correctly owns the current
> + * Verifies that UID 0 in the init namespace correctly owns the current
> * namespace when running in init_user_ns.
> *
> * @test: KUnit test context
> */
> -static void test_rootid_owns_currentns_init_ns(struct kunit *test)
> +static void test_uid_owns_currentns_init_ns(struct kunit *test)
> {
> - vfsuid_t root_vfsuid;
> - kuid_t root_kuid;
> + vfsuid_t vfsuid;
> + kuid_t kuid;
>
> - /* Create a root UID in init namespace */
> - root_kuid = KUIDT_INIT(0);
> - root_vfsuid = VFSUIDT_INIT(root_kuid);
> + /* Create UID 0 in init namespace */
> + kuid = KUIDT_INIT(0);
> + vfsuid = VFSUIDT_INIT(kuid);
>
> - /* In init namespace, root should own current namespace */
> - KUNIT_EXPECT_TRUE(test, rootid_owns_currentns(root_vfsuid));
> + /* In init namespace, UID 0 should own current namespace */
> + KUNIT_EXPECT_TRUE(test, uid_owns_currentns(vfsuid));
> }
>
> /**
> - * test_rootid_owns_currentns_invalid - Test rootid_owns_currentns with invalid vfsuid
> + * test_uid_owns_currentns_invalid - Test uid_owns_currentns with invalid vfsuid
> *
> * Verifies that an invalid vfsuid correctly returns false.
> *
> * @test: KUnit test context
> */
> -static void test_rootid_owns_currentns_invalid(struct kunit *test)
> +static void test_uid_owns_currentns_invalid(struct kunit *test)
> {
> vfsuid_t invalid_vfsuid;
>
> @@ -89,74 +91,268 @@ static void test_rootid_owns_currentns_invalid(struct kunit *test)
> invalid_vfsuid = INVALID_VFSUID;
>
> /* Invalid vfsuid should return false */
> - KUNIT_EXPECT_FALSE(test, rootid_owns_currentns(invalid_vfsuid));
> + KUNIT_EXPECT_FALSE(test, uid_owns_currentns(invalid_vfsuid));
> }
>
> /**
> - * test_rootid_owns_currentns_nonroot - Test rootid_owns_currentns with non-root UID
> + * test_uid_owns_currentns_nonzero - Test uid_owns_currentns with non-zero UID
> *
> - * Verifies that a non-root UID correctly returns false.
> + * Verifies that a non-zero UID correctly returns false.
> *
> * @test: KUnit test context
> */
> -static void test_rootid_owns_currentns_nonroot(struct kunit *test)
> +static void test_uid_owns_currentns_nonzero(struct kunit *test)
> {
> - vfsuid_t nonroot_vfsuid;
> - kuid_t nonroot_kuid;
> + vfsuid_t vfsuid;
> + kuid_t kuid;
>
> - /* Create a non-root UID */
> - nonroot_kuid = KUIDT_INIT(1000);
> - nonroot_vfsuid = VFSUIDT_INIT(nonroot_kuid);
> + /* Create a non-zero UID */
> + kuid = KUIDT_INIT(1000);
> + vfsuid = VFSUIDT_INIT(kuid);
>
> - /* Non-root UID should return false */
> - KUNIT_EXPECT_FALSE(test, rootid_owns_currentns(nonroot_vfsuid));
> + /* Non-zero UID should return false */
> + KUNIT_EXPECT_FALSE(test, uid_owns_currentns(vfsuid));
> }
>
> /**
> - * test_rootid_owns_userns_init_ns - Test rootid_owns_userns with init namespace
> + * test_uid_owns_ns_init_ns_uid0 - Test uid_owns_ns with init namespace and UID 0
> *
> - * Verifies that rootid_owns_userns correctly identifies root UID in init namespace.
> - * This tests the core namespace traversal logic.
> + * Verifies that uid_owns_ns correctly identifies UID 0 in init namespace.
> + * This tests the core namespace traversal logic. In init namespace, UID 0
> + * maps to itself, so it should own the namespace.
> *
> * @test: KUnit test context
> */
> -static void test_rootid_owns_userns_init_ns(struct kunit *test)
> +static void test_uid_owns_ns_init_ns_uid0(struct kunit *test)
> {
> - kuid_t root_kuid;
> + kuid_t kuid;
> struct user_namespace *init_ns;
>
> - root_kuid = KUIDT_INIT(0);
> + kuid = KUIDT_INIT(0);
> init_ns = &init_user_ns;
>
> - /* Root UID should own init namespace */
> - KUNIT_EXPECT_TRUE(test, rootid_owns_userns(init_ns, root_kuid));
> + /* UID 0 should own init namespace */
> + KUNIT_EXPECT_TRUE(test, uid_owns_ns(init_ns, kuid));
> }
>
> /**
> - * test_rootid_owns_userns_nonroot - Test rootid_owns_userns with non-root UID
> + * test_uid_owns_ns_init_ns_nonzero - Test uid_owns_ns with init namespace and non-zero UID
> *
> - * Verifies that rootid_owns_userns correctly rejects non-root UIDs.
> + * Verifies that uid_owns_ns correctly rejects non-zero UIDs in init namespace.
> + * Only UID 0 should own a namespace.
> *
> * @test: KUnit test context
> */
> -static void test_rootid_owns_userns_nonroot(struct kunit *test)
> +static void test_uid_owns_ns_init_ns_nonzero(struct kunit *test)
> {
> - kuid_t nonroot_kuid;
> + kuid_t kuid;
> struct user_namespace *init_ns;
>
> - nonroot_kuid = KUIDT_INIT(1000);
> + kuid = KUIDT_INIT(1000);
> init_ns = &init_user_ns;
>
> - /* Non-root UID should not own namespace */
> - KUNIT_EXPECT_FALSE(test, rootid_owns_userns(init_ns, nonroot_kuid));
> + /* Non-zero UID should not own namespace */
> + KUNIT_EXPECT_FALSE(test, uid_owns_ns(init_ns, kuid));
> +}
> +
> +/**
> + * test_uid_owns_ns_init_ns_various_uids - Test uid_owns_ns with various UIDs
> + *
> + * Verifies that uid_owns_ns correctly identifies only UID 0 as owning
> + * the namespace, regardless of the UID value tested.
> + *
> + * @test: KUnit test context
> + */
> +static void test_uid_owns_ns_init_ns_various_uids(struct kunit *test)
> +{
> + struct user_namespace *init_ns;
> + kuid_t kuid;
> +
> + init_ns = &init_user_ns;
> +
> + /* UID 0 should own the namespace */
> + kuid = KUIDT_INIT(0);
> + KUNIT_EXPECT_TRUE(test, uid_owns_ns(init_ns, kuid));
> +
> + /* Other UIDs should not own the namespace */
> + kuid = KUIDT_INIT(1);
> + KUNIT_EXPECT_FALSE(test, uid_owns_ns(init_ns, kuid));
> +
> + kuid = KUIDT_INIT(1000);
> + KUNIT_EXPECT_FALSE(test, uid_owns_ns(init_ns, kuid));
> +
> + kuid = KUIDT_INIT(65534);
> + KUNIT_EXPECT_FALSE(test, uid_owns_ns(init_ns, kuid));
> +}
> +
> +/**
> + * create_test_user_ns_with_mapping - Create a test user namespace with uid mapping
> + *
> + * Creates a minimal user namespace for testing where uid 0 in the namespace
> + * maps to the specified kuid in the parent namespace.
> + *
> + * The mapping semantics:
> + * - first: uid in this namespace (0)
> + * - lower_first: kuid in parent namespace (mapped_kuid)
> + * - count: range size (1)
> + *
> + * This means: from_kuid(ns, mapped_kuid) will return 0
> + * because map_id_up looks for kuid in [lower_first, lower_first+count)
> + * and returns first + (kuid - lower_first) = 0 + (mapped_kuid - mapped_kuid) = 0
> + *
> + * @test: KUnit test context
> + * @parent_ns: Parent user namespace
> + * @mapped_kuid: The kuid that uid 0 in the new namespace maps to
> + *
> + * Returns: The new user namespace, or NULL on failure
> + */
> +static struct user_namespace *create_test_user_ns_with_mapping(struct kunit *test,
> + struct user_namespace *parent_ns,
> + kuid_t mapped_kuid)
> +{
> + struct user_namespace *ns;
> + struct uid_gid_extent extent;
> +
> + /* Allocate a test namespace - use kzalloc to zero all fields */
> + ns = kunit_kzalloc(test, sizeof(*ns), GFP_KERNEL);
> + if (!ns)
> + return NULL;
> +
> + /* Initialize basic namespace structure fields */
> + ns->parent = parent_ns;
> + ns->level = parent_ns ? parent_ns->level + 1 : 0;
> + ns->owner = mapped_kuid;
> + ns->group = KGIDT_INIT(0);
> +
> + /* Initialize ns_common structure */
> + refcount_set(&ns->ns.__ns_ref, 1);
> +
> + /* Set up uid mapping: uid 0 in this namespace maps to mapped_kuid in parent
> + * Format: first (uid in ns) : lower_first (kuid in parent) : count
> + * So: uid 0 in ns -> kuid mapped_kuid in parent
> + * This means from_kuid(ns, mapped_kuid) returns 0
> + */
> + extent.first = 0; /* uid 0 in this namespace */
> + extent.lower_first = __kuid_val(mapped_kuid); /* maps to this kuid in parent */
> + extent.count = 1;
> +
> + ns->uid_map.extent[0] = extent;
> + ns->uid_map.nr_extents = 1;
> +
> + /* Set up gid mapping: gid 0 maps to gid 0 in parent (simplified) */
> + extent.first = 0;
> + extent.lower_first = 0;
> + extent.count = 1;
> +
> + ns->gid_map.extent[0] = extent;
> + ns->gid_map.nr_extents = 1;
> +
> + return ns;
> +}
> +
> +/**
> + * test_uid_owns_ns_with_mapping - Test uid_owns_ns with namespace where uid 0
> + * maps to different kuid
> + *
> + * Creates a user namespace where uid 0 maps to kuid 1000 in the parent namespace.
> + * Verifies that uid_owns_ns correctly identifies kuid 1000 as owning the namespace.
> + *
> + * Note: uid_owns_ns walks up the namespace hierarchy, so it checks the current
> + * namespace first, then parent, then parent's parent, etc. So:
> + * - kuid 1000 owns test_ns because from_kuid(test_ns, 1000) == 0
> + * - kuid 0 also owns test_ns because from_kuid(init_user_ns, 0) == 0
> + * (checked in parent)
> + *
> + * This tests the actual functionality as requested: creating namespaces with
> + * different values for the namespace's uid 0.
> + *
> + * @test: KUnit test context
> + */
> +static void test_uid_owns_ns_with_mapping(struct kunit *test)
> +{
> + struct user_namespace *test_ns;
> + struct user_namespace *parent_ns;
> + kuid_t mapped_kuid, other_kuid;
> +
> + parent_ns = &init_user_ns;
> + mapped_kuid = KUIDT_INIT(1000); /* uid 0 in test_ns maps to kuid 1000 */
> + other_kuid = KUIDT_INIT(2000); /* This should not own the namespace */
> +
> + /* Create test namespace where uid 0 maps to kuid 1000 */
> + test_ns = create_test_user_ns_with_mapping(test, parent_ns, mapped_kuid);
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, test_ns);
> +
> + /* kuid 1000 should own the namespace (because uid 0 in test_ns maps to it) */
> + KUNIT_EXPECT_TRUE(test, uid_owns_ns(test_ns, mapped_kuid));
> +
> + /* kuid 0 also owns the namespace because it maps to 0 in init_user_ns (parent) */
> + KUNIT_EXPECT_TRUE(test, uid_owns_ns(test_ns, KUIDT_INIT(0)));
> +
> + /* Other kuids that don't map to 0 in test_ns or any parent should not own */
> + KUNIT_EXPECT_FALSE(test, uid_owns_ns(test_ns, other_kuid));
> + KUNIT_EXPECT_FALSE(test, uid_owns_ns(test_ns, KUIDT_INIT(500)));
> +}
> +
> +/**
> + * test_uid_owns_ns_with_different_mappings - Test with multiple namespaces
> + * having different mappings
> + *
> + * Creates multiple test namespaces with different uid 0 mappings to verify
> + * the function correctly identifies ownership based on the mapping.
> + *
> + * Since uid_owns_ns walks up the hierarchy, kuids that map to 0 in init_user_ns
> + * (like kuid 0) will own all namespaces. But we can still verify that the
> + * specific mapped kuids own their respective namespaces.
> + *
> + * @test: KUnit test context
> + */
> +static void test_uid_owns_ns_with_different_mappings(struct kunit *test)
> +{
> + struct user_namespace *ns1, *ns2, *ns3;
> + struct user_namespace *parent_ns;
> +
> + parent_ns = &init_user_ns;
> +
> + /* Namespace 1: uid 0 maps to kuid 1000 */
> + ns1 = create_test_user_ns_with_mapping(test, parent_ns, KUIDT_INIT(1000));
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ns1);
> + /* kuid 1000 owns ns1 because it maps to uid 0 in ns1 */
> + KUNIT_EXPECT_TRUE(test, uid_owns_ns(ns1, KUIDT_INIT(1000)));
> + /* kuid 0 also owns ns1 because it maps to 0 in init_user_ns (parent) */
> + KUNIT_EXPECT_TRUE(test, uid_owns_ns(ns1, KUIDT_INIT(0)));
> + /* kuid 2000 doesn't map to 0 in ns1 or any parent */
> + KUNIT_EXPECT_FALSE(test, uid_owns_ns(ns1, KUIDT_INIT(2000)));
> +
> + /* Namespace 2: uid 0 maps to kuid 2000 */
> + ns2 = create_test_user_ns_with_mapping(test, parent_ns, KUIDT_INIT(2000));
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ns2);
> + /* kuid 2000 owns ns2 because it maps to uid 0 in ns2 */
> + KUNIT_EXPECT_TRUE(test, uid_owns_ns(ns2, KUIDT_INIT(2000)));
> + /* kuid 0 also owns ns2 because it maps to 0 in init_user_ns (parent) */
> + KUNIT_EXPECT_TRUE(test, uid_owns_ns(ns2, KUIDT_INIT(0)));
> + /* kuid 1000 doesn't map to 0 in ns2 or any parent */
> + KUNIT_EXPECT_FALSE(test, uid_owns_ns(ns2, KUIDT_INIT(1000)));
> +
> + /* Namespace 3: uid 0 maps to kuid 0 (identity mapping) */
> + ns3 = create_test_user_ns_with_mapping(test, parent_ns, KUIDT_INIT(0));
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ns3);
> + /* kuid 0 owns ns3 because it maps to uid 0 in ns3 */
> + KUNIT_EXPECT_TRUE(test, uid_owns_ns(ns3, KUIDT_INIT(0)));
> + /* kuid 1000 doesn't map to 0 in ns3 or any parent */
> + KUNIT_EXPECT_FALSE(test, uid_owns_ns(ns3, KUIDT_INIT(1000)));
> + /* kuid 2000 doesn't map to 0 in ns3 or any parent */
> + KUNIT_EXPECT_FALSE(test, uid_owns_ns(ns3, KUIDT_INIT(2000)));
> }
>
> static struct kunit_case commoncap_test_cases[] = {
> - KUNIT_CASE(test_rootid_owns_currentns_init_ns),
> - KUNIT_CASE(test_rootid_owns_currentns_invalid),
> - KUNIT_CASE(test_rootid_owns_currentns_nonroot),
> - KUNIT_CASE(test_rootid_owns_userns_init_ns),
> - KUNIT_CASE(test_rootid_owns_userns_nonroot),
> + KUNIT_CASE(test_uid_owns_currentns_init_ns),
> + KUNIT_CASE(test_uid_owns_currentns_invalid),
> + KUNIT_CASE(test_uid_owns_currentns_nonzero),
> + KUNIT_CASE(test_uid_owns_ns_init_ns_uid0),
> + KUNIT_CASE(test_uid_owns_ns_init_ns_nonzero),
> + KUNIT_CASE(test_uid_owns_ns_init_ns_various_uids),
> + KUNIT_CASE(test_uid_owns_ns_with_mapping),
> + KUNIT_CASE(test_uid_owns_ns_with_different_mappings),
> {}
> };
>
> --
> 2.43.0
^ permalink raw reply
* Re: [RFC][PATCH] exec: Move cred computation under exec_update_lock
From: Bernd Edlinger @ 2025-11-25 16:19 UTC (permalink / raw)
To: Eric W. Biederman, Oleg Nesterov
Cc: Alexander Viro, Alexey Dobriyan, Kees Cook, Andy Lutomirski,
Will Drewry, Christian Brauner, Andrew Morton, Michal Hocko,
Serge Hallyn, James Morris, Randy Dunlap, Suren Baghdasaryan,
Yafang Shao, Helge Deller, Adrian Reber, Thomas Gleixner,
Jens Axboe, Alexei Starovoitov, linux-fsdevel@vger.kernel.org,
linux-kernel@vger.kernel.org, linux-kselftest, linux-mm,
linux-security-module, tiozhang, Luis Chamberlain,
Paulo Alcantara (SUSE), Sergey Senozhatsky, Frederic Weisbecker,
YueHaibing, Paul Moore, Aleksa Sarai, Stefan Roesch, Chao Yu,
xu xin, Jeff Layton, Jan Kara, David Hildenbrand, Dave Chinner,
Shuah Khan, Elena Reshetova, David Windsor, Mateusz Guzik,
Ard Biesheuvel, Joel Fernandes (Google), Matthew Wilcox (Oracle),
Hans Liljestrand, Penglei Jiang, Lorenzo Stoakes, Adrian Ratiu,
Ingo Molnar, Peter Zijlstra (Intel), Cyrill Gorcunov,
Eric Dumazet
In-Reply-To: <87tsykuyf7.fsf@email.froward.int.ebiederm.org>
On 11/24/25 00:22, Eric W. Biederman wrote:
> Oleg Nesterov <oleg@redhat.com> writes:
>
>> Eric,
>>
>> sorry for delay, I am on PTO, didn't read emails this week...
>>
>> On 11/20, Eric W. Biederman wrote:
>>>
>>> Instead of computing the new cred before we pass the point of no
>>> return compute the new cred just before we use it.
>>>
>>> This allows the removal of fs_struct->in_exec and cred_guard_mutex.
>>>
>>> I am not certain why we wanted to compute the cred for the new
>>> executable so early. Perhaps I missed something but I did not see any
>>> common errors being signaled. So I don't think we loose anything by
>>> computing the new cred later.
>>>
>>> We gain a lot.
>>
>> Yes. I LIKE your approach after a quick glance. And I swear, I thought about
>> it too ;)
>>
>> But is it correct? I don't know. I'll try to actually read your patch next
>> week (I am on PTO untill the end of November), but I am not sure I can
>> provide a valuable feedback.
>>
>> One "obvious" problem is that, after this patch, the execing process can crash
>> in a case when currently exec() returns an error...
>
> Yes.
>
> I have been testing and looking at it, and I have found a few issues,
> and I am trying to see if I can resolve them.
>
> The good news is that with the advent of AT_EXECVE_CHECK we have a
> really clear API boundary between errors that must be diagnosed
> and errors of happenstance like running out of memory.
>
> The bad news is that the implementation of AT_EXECVE_CHECK seems to been
> rather hackish especially with respect to security_bprm_creds_for_exec.
>
> What I am hoping for is to get the 3 causes of errors of brpm->unsafe
> ( LSM_UNSAFE_SHARE, LSM_UNSAFE_PTRACE, and LSM_UNSAFE_NO_NEW_PRIVS )
> handled cleanly outside of the cred_guard_mutex, and simply
> retested when it is time to build the credentials of the new process.
>
> In practice that should get the same failures modes as we have now
> but it would get SIGSEGV in rare instances where things changed
> during exec. That feels acceptable.
>
>
>
> I thought of one other approach that might be enough to put the issue to
> bed if cleaning up exec is too much work. We could have ptrace_attach
> use a trylock and fail when it doesn't succeed. That would solve the
> worst of the symptoms.
>
> I think this would be a complete patch:
>
> diff --git a/kernel/ptrace.c b/kernel/ptrace.c
> index 75a84efad40f..5dd2144e5789 100644
> --- a/kernel/ptrace.c
> +++ b/kernel/ptrace.c
> @@ -444,7 +444,7 @@ static int ptrace_attach(struct task_struct *task, long request,
> * SUID, SGID and LSM creds get determined differently
> * under ptrace.
> */
> - scoped_cond_guard (mutex_intr, return -ERESTARTNOINTR,
> + scoped_cond_guard (mutex_try, return -EAGAIN,
> &task->signal->cred_guard_mutex) {
>
> scoped_guard (task_lock, task) {
This is very similar to my initial attempt of fixing the problem, as you
can see the test expectaion of the currently failing test in vmattach.c
is that ptrace(PTRACE_ATTACH, pid, 0L, 0L) returns -1 with errno = EAGAIN.
The disadvantage of that approach was, that it is a user-visible API-change,
but also that the debugger does not know when to retry the PTRACE_ATTACH,
in worst case it will go into an endless loop not knowing that a waitpid
and/or PTRACE_CONT is necessary to unblock the traced process.
But The main reason why I preferred the overlapping lifetime of the current
and the new credentials, is that the tracee can escape the PTRACE_ATTACH
if it is very short-lived, and indeed I had to cheat a little to make the
test case function TEST(attach) pass reliably:
The traced process does execlp("sleep", "sleep", "2", NULL);
If it did execlp("true", "true", NULL); like the first test case, it would
have failed randomly, because the debugger could not attach quickly enoguh,
and IMHO the expectaion of the debugger is probably to be able to stop the
new process at the first instruction after the execve.
Bernd.
^ permalink raw reply
* Re: [PATCH 0/2] apparmor unaligned memory fixes
From: John Johansen @ 2025-11-25 19:20 UTC (permalink / raw)
To: Helge Deller, John Paul Adrian Glaubitz
Cc: Helge Deller, linux-kernel, apparmor, linux-security-module,
linux-parisc
In-Reply-To: <aSXHCyH_rS-c5BgP@p100>
On 11/25/25 07:11, Helge Deller wrote:
> * John Johansen <john.johansen@canonical.com>:
>> On 11/18/25 04:49, Helge Deller wrote:
>>> Hi Adrian,
>>>
>>> On 11/18/25 12:43, John Paul Adrian Glaubitz wrote:
>>>> On Tue, 2025-11-18 at 12:09 +0100, Helge Deller wrote:
>>>>> My patch fixed two call sites, but I suspect you see another call site which
>>>>> hasn't been fixed yet.
>>>>>
>>>>> Can you try attached patch? It might indicate the caller of the function and
>>>>> maybe prints the struct name/address which isn't aligned.
>>>>>
>>>>> Helge
>>>>>
>>>>>
>>>>> diff --git a/security/apparmor/match.c b/security/apparmor/match.c
>>>>> index c5a91600842a..b477430c07eb 100644
>>>>> --- a/security/apparmor/match.c
>>>>> +++ b/security/apparmor/match.c
>>>>> @@ -313,6 +313,9 @@ struct aa_dfa *aa_dfa_unpack(void *blob, size_t size, int flags)
>>>>> if (size < sizeof(struct table_set_header))
>>>>> goto fail;
>>>>> + if (WARN_ON(((unsigned long)data) & (BITS_PER_LONG/8 - 1)))
>>>>> + pr_warn("dfa blob stream %pS not aligned.\n", data);
>>>>> +
>>>>> if (ntohl(*(__be32 *) data) != YYTH_MAGIC)
>>>>> goto fail;
>>>>
>>>> Here is the relevant output with the patch applied:
>>>>
>>>> [ 73.840639] ------------[ cut here ]------------
>>>> [ 73.901376] WARNING: CPU: 0 PID: 341 at security/apparmor/match.c:316 aa_dfa_unpack+0x6cc/0x720
>>>> [ 74.015867] Modules linked in: binfmt_misc evdev flash sg drm drm_panel_orientation_quirks backlight i2c_core configfs nfnetlink autofs4 ext4 crc16 mbcache jbd2 hid_generic usbhid sr_mod hid cdrom
>>>> sd_mod ata_generic ohci_pci ehci_pci ehci_hcd ohci_hcd pata_ali libata sym53c8xx scsi_transport_spi tg3 scsi_mod usbcore libphy scsi_common mdio_bus usb_common
>>>> [ 74.428977] CPU: 0 UID: 0 PID: 341 Comm: apparmor_parser Not tainted 6.18.0-rc6+ #9 NONE
>>>> [ 74.536543] Call Trace:
>>>> [ 74.568561] [<0000000000434c24>] dump_stack+0x8/0x18
>>>> [ 74.633757] [<0000000000476438>] __warn+0xd8/0x100
>>>> [ 74.696664] [<00000000004296d4>] warn_slowpath_fmt+0x34/0x74
>>>> [ 74.771006] [<00000000008db28c>] aa_dfa_unpack+0x6cc/0x720
>>>> [ 74.843062] [<00000000008e643c>] unpack_pdb+0xbc/0x7e0
>>>> [ 74.910545] [<00000000008e7740>] unpack_profile+0xbe0/0x1300
>>>> [ 74.984888] [<00000000008e82e0>] aa_unpack+0xe0/0x6a0
>>>> [ 75.051226] [<00000000008e3ec4>] aa_replace_profiles+0x64/0x1160
>>>> [ 75.130144] [<00000000008d4d90>] policy_update+0xf0/0x280
>>>> [ 75.201057] [<00000000008d4fc8>] profile_replace+0xa8/0x100
>>>> [ 75.274258] [<0000000000766bd0>] vfs_write+0x90/0x420
>>>> [ 75.340594] [<00000000007670cc>] ksys_write+0x4c/0xe0
>>>> [ 75.406932] [<0000000000767174>] sys_write+0x14/0x40
>>>> [ 75.472126] [<0000000000406174>] linux_sparc_syscall+0x34/0x44
>>>> [ 75.548802] ---[ end trace 0000000000000000 ]---
>>>> [ 75.609503] dfa blob stream 0xfff0000008926b96 not aligned.
>>>> [ 75.682695] Kernel unaligned access at TPC[8db2a8] aa_dfa_unpack+0x6e8/0x720
>>>
>>> The non-8-byte-aligned address (0xfff0000008926b96) is coming from userspace
>>> (via the write syscall).
>>> Some apparmor userspace tool writes into the apparmor ".replace" virtual file with
>>> a source address which is not correctly aligned.
>>
>> the userpace buffer passed to write(2) has to be aligned? Its certainly nice if it
>> is but the userspace tooling hasn't been treating it as aligned. With that said,
>> the dfa should be padded to be aligned. So this tripping in the dfa is a bug,
>> and there really should be some validation to catch it.
>>
>>> You should be able to debug/find the problematic code with strace from userspace.
>>> Maybe someone with apparmor knowledge here on the list has an idea?
>>>
>> This is likely an unaligned 2nd profile, being split out and loaded separately
>> from the rest of the container. Basically the loader for some reason (there
>> are a few different possible reasons) is poking into the container format and
>> pulling out the profile at some offset, this gets loaded to the kernel but
>> it would seem that its causing an issue with the dfa alignment within the container,
>> which should be aligned to the original container.
>
>
> Regarding this:
>
>> Kernel side, we are going to need to add some extra verification checks, it should
>> be catching this, as unaligned as part of the unpack. Userspace side, we will have
>> to verify my guess and fix the loader.
>
> I wonder if loading those tables are really time critical?
no, most policy is loaded once on boot and then at package upgrades. There are some
bits that may be loaded at application startup like, snap, libvirt, lxd, basically
container managers might do some thing custom per container.
Its the run time we want to minimize, the cost of.
Policy already can be unaligned (the container format rework to fix this is low
priority), and is treated as untrusted. It goes through an unpack, and translation to
machine native, with as many bounds checks, necessary transforms etc done at unpack
time as possible, so that the run time costs can be minimized.
> If not, maybe just making the kernel aware that the tables might be unaligned
> can help, e.g. with the following (untested) patch.
> Adrian, maybe you want to test?
>
> ------------------------
>
> [PATCH] Allow apparmor to handle unaligned dfa tables
>
> The dfa tables can originate from kernel or userspace and 8-byte alignment
> isn't always guaranteed and as such may trigger unaligned memory accesses
> on various architectures.
> Work around it by using the get_unaligned_xx() helpers.
>
> Signed-off-by: Helge Deller <deller@gmx.de>
>
lgtm,
Acked-by: John Johansen <john.johansen@canonical.com>
I'll pull this into my tree regardless of whether it fixes the issue
for Adrian, as it definitely fixes an issue.
We can added additional patches on top s needed.
> diff --git a/security/apparmor/match.c b/security/apparmor/match.c
> index c5a91600842a..26e82ba879d4 100644
> --- a/security/apparmor/match.c
> +++ b/security/apparmor/match.c
> @@ -15,6 +15,7 @@
> #include <linux/vmalloc.h>
> #include <linux/err.h>
> #include <linux/kref.h>
> +#include <linux/unaligned.h>
>
> #include "include/lib.h"
> #include "include/match.h"
> @@ -42,11 +43,11 @@ static struct table_header *unpack_table(char *blob, size_t bsize)
> /* loaded td_id's start at 1, subtract 1 now to avoid doing
> * it every time we use td_id as an index
> */
> - th.td_id = be16_to_cpu(*(__be16 *) (blob)) - 1;
> + th.td_id = get_unaligned_be16(blob) - 1;
> if (th.td_id > YYTD_ID_MAX)
> goto out;
> - th.td_flags = be16_to_cpu(*(__be16 *) (blob + 2));
> - th.td_lolen = be32_to_cpu(*(__be32 *) (blob + 8));
> + th.td_flags = get_unaligned_be16(blob + 2);
> + th.td_lolen = get_unaligned_be32(blob + 8);
> blob += sizeof(struct table_header);
>
> if (!(th.td_flags == YYTD_DATA16 || th.td_flags == YYTD_DATA32 ||
> @@ -313,14 +314,14 @@ struct aa_dfa *aa_dfa_unpack(void *blob, size_t size, int flags)
> if (size < sizeof(struct table_set_header))
> goto fail;
>
> - if (ntohl(*(__be32 *) data) != YYTH_MAGIC)
> + if (get_unaligned_be32(data) != YYTH_MAGIC)
> goto fail;
>
> - hsize = ntohl(*(__be32 *) (data + 4));
> + hsize = get_unaligned_be32(data + 4);
> if (size < hsize)
> goto fail;
>
> - dfa->flags = ntohs(*(__be16 *) (data + 12));
> + dfa->flags = get_unaligned_be16(data + 12);
> if (dfa->flags & ~(YYTH_FLAGS))
> goto fail;
>
> @@ -329,7 +330,7 @@ struct aa_dfa *aa_dfa_unpack(void *blob, size_t size, int flags)
> * if (dfa->flags & YYTH_FLAGS_OOB_TRANS) {
> * if (hsize < 16 + 4)
> * goto fail;
> - * dfa->max_oob = ntol(*(__be32 *) (data + 16));
> + * dfa->max_oob = get_unaligned_be32(data + 16);
> * if (dfa->max <= MAX_OOB_SUPPORTED) {
> * pr_err("AppArmor DFA OOB greater than supported\n");
> * goto fail;
^ permalink raw reply
* Re: [PATCH 0/2] apparmor unaligned memory fixes
From: Helge Deller @ 2025-11-25 21:13 UTC (permalink / raw)
To: John Johansen, Helge Deller, John Paul Adrian Glaubitz
Cc: linux-kernel, apparmor, linux-security-module, linux-parisc
In-Reply-To: <e88c32c2-fb18-4f3e-9ec2-a749695aaf0a@canonical.com>
On 11/25/25 20:20, John Johansen wrote:
> On 11/25/25 07:11, Helge Deller wrote:
>> * John Johansen <john.johansen@canonical.com>:
>>> On 11/18/25 04:49, Helge Deller wrote:
>>>> Hi Adrian,
>>>>
>>>> On 11/18/25 12:43, John Paul Adrian Glaubitz wrote:
>>>>> On Tue, 2025-11-18 at 12:09 +0100, Helge Deller wrote:
>>>>>> My patch fixed two call sites, but I suspect you see another call site which
>>>>>> hasn't been fixed yet.
>>>>>>
>>>>>> Can you try attached patch? It might indicate the caller of the function and
>>>>>> maybe prints the struct name/address which isn't aligned.
>>>>>>
>>>>>> Helge
>>>>>>
>>>>>>
>>>>>> diff --git a/security/apparmor/match.c b/security/apparmor/match.c
>>>>>> index c5a91600842a..b477430c07eb 100644
>>>>>> --- a/security/apparmor/match.c
>>>>>> +++ b/security/apparmor/match.c
>>>>>> @@ -313,6 +313,9 @@ struct aa_dfa *aa_dfa_unpack(void *blob, size_t size, int flags)
>>>>>> if (size < sizeof(struct table_set_header))
>>>>>> goto fail;
>>>>>> + if (WARN_ON(((unsigned long)data) & (BITS_PER_LONG/8 - 1)))
>>>>>> + pr_warn("dfa blob stream %pS not aligned.\n", data);
>>>>>> +
>>>>>> if (ntohl(*(__be32 *) data) != YYTH_MAGIC)
>>>>>> goto fail;
>>>>>
>>>>> Here is the relevant output with the patch applied:
>>>>>
>>>>> [ 73.840639] ------------[ cut here ]------------
>>>>> [ 73.901376] WARNING: CPU: 0 PID: 341 at security/apparmor/match.c:316 aa_dfa_unpack+0x6cc/0x720
>>>>> [ 74.015867] Modules linked in: binfmt_misc evdev flash sg drm drm_panel_orientation_quirks backlight i2c_core configfs nfnetlink autofs4 ext4 crc16 mbcache jbd2 hid_generic usbhid sr_mod hid cdrom
>>>>> sd_mod ata_generic ohci_pci ehci_pci ehci_hcd ohci_hcd pata_ali libata sym53c8xx scsi_transport_spi tg3 scsi_mod usbcore libphy scsi_common mdio_bus usb_common
>>>>> [ 74.428977] CPU: 0 UID: 0 PID: 341 Comm: apparmor_parser Not tainted 6.18.0-rc6+ #9 NONE
>>>>> [ 74.536543] Call Trace:
>>>>> [ 74.568561] [<0000000000434c24>] dump_stack+0x8/0x18
>>>>> [ 74.633757] [<0000000000476438>] __warn+0xd8/0x100
>>>>> [ 74.696664] [<00000000004296d4>] warn_slowpath_fmt+0x34/0x74
>>>>> [ 74.771006] [<00000000008db28c>] aa_dfa_unpack+0x6cc/0x720
>>>>> [ 74.843062] [<00000000008e643c>] unpack_pdb+0xbc/0x7e0
>>>>> [ 74.910545] [<00000000008e7740>] unpack_profile+0xbe0/0x1300
>>>>> [ 74.984888] [<00000000008e82e0>] aa_unpack+0xe0/0x6a0
>>>>> [ 75.051226] [<00000000008e3ec4>] aa_replace_profiles+0x64/0x1160
>>>>> [ 75.130144] [<00000000008d4d90>] policy_update+0xf0/0x280
>>>>> [ 75.201057] [<00000000008d4fc8>] profile_replace+0xa8/0x100
>>>>> [ 75.274258] [<0000000000766bd0>] vfs_write+0x90/0x420
>>>>> [ 75.340594] [<00000000007670cc>] ksys_write+0x4c/0xe0
>>>>> [ 75.406932] [<0000000000767174>] sys_write+0x14/0x40
>>>>> [ 75.472126] [<0000000000406174>] linux_sparc_syscall+0x34/0x44
>>>>> [ 75.548802] ---[ end trace 0000000000000000 ]---
>>>>> [ 75.609503] dfa blob stream 0xfff0000008926b96 not aligned.
>>>>> [ 75.682695] Kernel unaligned access at TPC[8db2a8] aa_dfa_unpack+0x6e8/0x720
>>>>
>>>> The non-8-byte-aligned address (0xfff0000008926b96) is coming from userspace
>>>> (via the write syscall).
>>>> Some apparmor userspace tool writes into the apparmor ".replace" virtual file with
>>>> a source address which is not correctly aligned.
>>>
>>> the userpace buffer passed to write(2) has to be aligned? Its certainly nice if it
>>> is but the userspace tooling hasn't been treating it as aligned. With that said,
>>> the dfa should be padded to be aligned. So this tripping in the dfa is a bug,
>>> and there really should be some validation to catch it.
>>>
>>>> You should be able to debug/find the problematic code with strace from userspace.
>>>> Maybe someone with apparmor knowledge here on the list has an idea?
>>>>
>>> This is likely an unaligned 2nd profile, being split out and loaded separately
>>> from the rest of the container. Basically the loader for some reason (there
>>> are a few different possible reasons) is poking into the container format and
>>> pulling out the profile at some offset, this gets loaded to the kernel but
>>> it would seem that its causing an issue with the dfa alignment within the container,
>>> which should be aligned to the original container.
>>
>>
>> Regarding this:
>>
>>> Kernel side, we are going to need to add some extra verification checks, it should
>>> be catching this, as unaligned as part of the unpack. Userspace side, we will have
>>> to verify my guess and fix the loader.
>>
>> I wonder if loading those tables are really time critical?
>
> no, most policy is loaded once on boot and then at package upgrades. There are some
> bits that may be loaded at application startup like, snap, libvirt, lxd, basically
> container managers might do some thing custom per container.
>
> Its the run time we want to minimize, the cost of.
>
> Policy already can be unaligned (the container format rework to fix this is low
> priority), and is treated as untrusted. It goes through an unpack, and translation to
> machine native, with as many bounds checks, necessary transforms etc done at unpack
> time as possible, so that the run time costs can be minimized.
>
>> If not, maybe just making the kernel aware that the tables might be unaligned
>> can help, e.g. with the following (untested) patch.
>> Adrian, maybe you want to test?
>>
>
>> ------------------------
>>
>> [PATCH] Allow apparmor to handle unaligned dfa tables
>>
>> The dfa tables can originate from kernel or userspace and 8-byte alignment
>> isn't always guaranteed and as such may trigger unaligned memory accesses
>> on various architectures.
>> Work around it by using the get_unaligned_xx() helpers.
>>
>> Signed-off-by: Helge Deller <deller@gmx.de>
>>
> lgtm,
>
> Acked-by: John Johansen <john.johansen@canonical.com>
>
> I'll pull this into my tree regardless of whether it fixes the issue
> for Adrian, as it definitely fixes an issue.
>
> We can added additional patches on top s needed.
My patch does not modify the UNPACK_ARRAY() macro, which we
possibly should adjust as well.
Adrian's testing seems to trigger only a few unaligned accesses,
so maybe it's not a issue currently.
Helge
^ permalink raw reply
* Re: [PATCH 0/2] apparmor unaligned memory fixes
From: John Paul Adrian Glaubitz @ 2025-11-26 7:27 UTC (permalink / raw)
To: Helge Deller, John Johansen
Cc: Helge Deller, linux-kernel, apparmor, linux-security-module,
linux-parisc
In-Reply-To: <aSXHCyH_rS-c5BgP@p100>
Hi Helge,
On Tue, 2025-11-25 at 16:11 +0100, Helge Deller wrote:
> Regarding this:
>
> > Kernel side, we are going to need to add some extra verification checks, it should
> > be catching this, as unaligned as part of the unpack. Userspace side, we will have
> > to verify my guess and fix the loader.
>
> I wonder if loading those tables are really time critical?
> If not, maybe just making the kernel aware that the tables might be unaligned
> can help, e.g. with the following (untested) patch.
> Adrian, maybe you want to test?
Yes, I'll test that one.
> ------------------------
>
> [PATCH] Allow apparmor to handle unaligned dfa tables
>
> The dfa tables can originate from kernel or userspace and 8-byte alignment
> isn't always guaranteed and as such may trigger unaligned memory accesses
> on various architectures.
> Work around it by using the get_unaligned_xx() helpers.
>
> Signed-off-by: Helge Deller <deller@gmx.de>
>
> diff --git a/security/apparmor/match.c b/security/apparmor/match.c
> index c5a91600842a..26e82ba879d4 100644
> --- a/security/apparmor/match.c
> +++ b/security/apparmor/match.c
> @@ -15,6 +15,7 @@
> #include <linux/vmalloc.h>
> #include <linux/err.h>
> #include <linux/kref.h>
> +#include <linux/unaligned.h>
>
> #include "include/lib.h"
> #include "include/match.h"
> @@ -42,11 +43,11 @@ static struct table_header *unpack_table(char *blob, size_t bsize)
> /* loaded td_id's start at 1, subtract 1 now to avoid doing
> * it every time we use td_id as an index
> */
> - th.td_id = be16_to_cpu(*(__be16 *) (blob)) - 1;
> + th.td_id = get_unaligned_be16(blob) - 1;
> if (th.td_id > YYTD_ID_MAX)
> goto out;
> - th.td_flags = be16_to_cpu(*(__be16 *) (blob + 2));
> - th.td_lolen = be32_to_cpu(*(__be32 *) (blob + 8));
> + th.td_flags = get_unaligned_be16(blob + 2);
> + th.td_lolen = get_unaligned_be32(blob + 8);
> blob += sizeof(struct table_header);
>
> if (!(th.td_flags == YYTD_DATA16 || th.td_flags == YYTD_DATA32 ||
> @@ -313,14 +314,14 @@ struct aa_dfa *aa_dfa_unpack(void *blob, size_t size, int flags)
> if (size < sizeof(struct table_set_header))
> goto fail;
>
> - if (ntohl(*(__be32 *) data) != YYTH_MAGIC)
> + if (get_unaligned_be32(data) != YYTH_MAGIC)
> goto fail;
>
> - hsize = ntohl(*(__be32 *) (data + 4));
> + hsize = get_unaligned_be32(data + 4);
> if (size < hsize)
> goto fail;
>
> - dfa->flags = ntohs(*(__be16 *) (data + 12));
> + dfa->flags = get_unaligned_be16(data + 12);
> if (dfa->flags & ~(YYTH_FLAGS))
> goto fail;
>
> @@ -329,7 +330,7 @@ struct aa_dfa *aa_dfa_unpack(void *blob, size_t size, int flags)
> * if (dfa->flags & YYTH_FLAGS_OOB_TRANS) {
> * if (hsize < 16 + 4)
> * goto fail;
> - * dfa->max_oob = ntol(*(__be32 *) (data + 16));
> + * dfa->max_oob = get_unaligned_be32(data + 16);
> * if (dfa->max <= MAX_OOB_SUPPORTED) {
> * pr_err("AppArmor DFA OOB greater than supported\n");
> * goto fail;
Adrian
--
.''`. John Paul Adrian Glaubitz
: :' : Debian Developer
`. `' Physicist
`- GPG: 62FF 8A75 84E0 2956 9546 0006 7426 3B37 F5B5 F913
^ permalink raw reply
* Re: [PATCH 0/2] apparmor unaligned memory fixes
From: John Paul Adrian Glaubitz @ 2025-11-26 7:52 UTC (permalink / raw)
To: Helge Deller, John Johansen
Cc: Helge Deller, linux-kernel, apparmor, linux-security-module,
linux-parisc
In-Reply-To: <aSXHCyH_rS-c5BgP@p100>
Hi Helge,
On Tue, 2025-11-25 at 16:11 +0100, Helge Deller wrote:
> Regarding this:
>
> > Kernel side, we are going to need to add some extra verification checks, it should
> > be catching this, as unaligned as part of the unpack. Userspace side, we will have
> > to verify my guess and fix the loader.
>
> I wonder if loading those tables are really time critical?
> If not, maybe just making the kernel aware that the tables might be unaligned
> can help, e.g. with the following (untested) patch.
> Adrian, maybe you want to test?
>
> ------------------------
>
> [PATCH] Allow apparmor to handle unaligned dfa tables
>
> The dfa tables can originate from kernel or userspace and 8-byte alignment
> isn't always guaranteed and as such may trigger unaligned memory accesses
> on various architectures.
> Work around it by using the get_unaligned_xx() helpers.
>
> Signed-off-by: Helge Deller <deller@gmx.de>
>
> diff --git a/security/apparmor/match.c b/security/apparmor/match.c
> index c5a91600842a..26e82ba879d4 100644
> --- a/security/apparmor/match.c
> +++ b/security/apparmor/match.c
> @@ -15,6 +15,7 @@
> #include <linux/vmalloc.h>
> #include <linux/err.h>
> #include <linux/kref.h>
> +#include <linux/unaligned.h>
>
> #include "include/lib.h"
> #include "include/match.h"
> @@ -42,11 +43,11 @@ static struct table_header *unpack_table(char *blob, size_t bsize)
> /* loaded td_id's start at 1, subtract 1 now to avoid doing
> * it every time we use td_id as an index
> */
> - th.td_id = be16_to_cpu(*(__be16 *) (blob)) - 1;
> + th.td_id = get_unaligned_be16(blob) - 1;
> if (th.td_id > YYTD_ID_MAX)
> goto out;
> - th.td_flags = be16_to_cpu(*(__be16 *) (blob + 2));
> - th.td_lolen = be32_to_cpu(*(__be32 *) (blob + 8));
> + th.td_flags = get_unaligned_be16(blob + 2);
> + th.td_lolen = get_unaligned_be32(blob + 8);
> blob += sizeof(struct table_header);
>
> if (!(th.td_flags == YYTD_DATA16 || th.td_flags == YYTD_DATA32 ||
> @@ -313,14 +314,14 @@ struct aa_dfa *aa_dfa_unpack(void *blob, size_t size, int flags)
> if (size < sizeof(struct table_set_header))
> goto fail;
>
> - if (ntohl(*(__be32 *) data) != YYTH_MAGIC)
> + if (get_unaligned_be32(data) != YYTH_MAGIC)
> goto fail;
>
> - hsize = ntohl(*(__be32 *) (data + 4));
> + hsize = get_unaligned_be32(data + 4);
> if (size < hsize)
> goto fail;
>
> - dfa->flags = ntohs(*(__be16 *) (data + 12));
> + dfa->flags = get_unaligned_be16(data + 12);
> if (dfa->flags & ~(YYTH_FLAGS))
> goto fail;
>
> @@ -329,7 +330,7 @@ struct aa_dfa *aa_dfa_unpack(void *blob, size_t size, int flags)
> * if (dfa->flags & YYTH_FLAGS_OOB_TRANS) {
> * if (hsize < 16 + 4)
> * goto fail;
> - * dfa->max_oob = ntol(*(__be32 *) (data + 16));
> + * dfa->max_oob = get_unaligned_be32(data + 16);
> * if (dfa->max <= MAX_OOB_SUPPORTED) {
> * pr_err("AppArmor DFA OOB greater than supported\n");
> * goto fail;
I can confirm that this fixes the unaligned access warnings.
Without the patch:
[ 72.073526] audit: type=1400 audit(1764145307.711:2): apparmor="STATUS" operation="profile_load" profile="unconfined" name="1password" pid=292 comm="apparmor_parser"
[ 72.413269] audit: type=1400 audit(1764145308.051:3): apparmor="STATUS" operation="profile_load" profile="unconfined" name="Discord" pid=294 comm="apparmor_parser"
[ 72.645135] audit: type=1400 audit(1764145308.283:4): apparmor="STATUS" operation="profile_load" profile="unconfined" name=4D6F6E676F444220436F6D70617373 pid=296 comm="apparmor_parser"
[ 72.901297] audit: type=1400 audit(1764145308.539:5): apparmor="STATUS" operation="profile_load" profile="unconfined" name="QtWebEngineProcess" pid=297 comm="apparmor_parser"
[ 73.245252] audit: type=1400 audit(1764145308.879:6): apparmor="STATUS" operation="profile_load" profile="unconfined" name="Xorg" pid=298 comm="apparmor_parser"
[ 73.468571] audit: type=1400 audit(1764145309.107:7): apparmor="STATUS" operation="profile_load" profile="unconfined" name="balena-etcher" pid=299 comm="apparmor_parser"
[ 73.688642] audit: type=1400 audit(1764145309.327:8): apparmor="STATUS" operation="profile_load" profile="unconfined" name="brave" pid=300 comm="apparmor_parser"
[ 73.897068] audit: type=1400 audit(1764145309.531:9): apparmor="STATUS" operation="profile_load" profile="unconfined" name="buildah" pid=301 comm="apparmor_parser"
[ 74.104434] audit: type=1400 audit(1764145309.739:10): apparmor="STATUS" operation="profile_load" profile="unconfined" name="busybox" pid=302 comm="apparmor_parser"
[ 74.313359] audit: type=1400 audit(1764145309.951:11): apparmor="STATUS" operation="profile_load" profile="unconfined" name="cam" pid=303 comm="apparmor_parser"
[ 74.808437] Kernel unaligned access at TPC[8dabdc] aa_dfa_unpack+0x3c/0x6e0
[ 74.900032] Kernel unaligned access at TPC[8dabec] aa_dfa_unpack+0x4c/0x6e0
[ 74.991608] Kernel unaligned access at TPC[8dacd0] aa_dfa_unpack+0x130/0x6e0
[ 75.084339] Kernel unaligned access at TPC[8dacd0] aa_dfa_unpack+0x130/0x6e0
[ 75.176997] Kernel unaligned access at TPC[8dacd0] aa_dfa_unpack+0x130/0x6e0
With the patch:
[ 78.058157] audit: type=1400 audit(1764145018.691:2): apparmor="STATUS" operation="profile_load" profile="unconfined" name="1password" pid=294 comm="apparmor_parser"
[ 78.294742] audit: type=1400 audit(1764145018.927:3): apparmor="STATUS" operation="profile_load" profile="unconfined" name="Discord" pid=295 comm="apparmor_parser"
[ 78.516989] audit: type=1400 audit(1764145019.127:4): apparmor="STATUS" operation="profile_load" profile="unconfined" name=4D6F6E676F444220436F6D70617373 pid=297 comm="apparmor_parser"
[ 78.748842] audit: type=1400 audit(1764145019.379:5): apparmor="STATUS" operation="profile_load" profile="unconfined" name="QtWebEngineProcess" pid=298 comm="apparmor_parser"
[ 79.101544] audit: type=1400 audit(1764145019.731:6): apparmor="STATUS" operation="profile_load" profile="unconfined" name="Xorg" pid=299 comm="apparmor_parser"
[ 79.335655] audit: type=1400 audit(1764145019.967:7): apparmor="STATUS" operation="profile_load" profile="unconfined" name="balena-etcher" pid=300 comm="apparmor_parser"
[ 79.559475] audit: type=1400 audit(1764145020.191:8): apparmor="STATUS" operation="profile_load" profile="unconfined" name="brave" pid=301 comm="apparmor_parser"
[ 79.768389] audit: type=1400 audit(1764145020.399:9): apparmor="STATUS" operation="profile_load" profile="unconfined" name="buildah" pid=302 comm="apparmor_parser"
[ 79.974008] audit: type=1400 audit(1764145020.607:10): apparmor="STATUS" operation="profile_load" profile="unconfined" name="busybox" pid=303 comm="apparmor_parser"
[ 80.194378] audit: type=1400 audit(1764145020.827:11): apparmor="STATUS" operation="profile_load" profile="unconfined" name="cam" pid=304 comm="apparmor_parser"
So, it seems your approach works as expected.
Tested-by: John Paul Adrian Glaubitz <glaubitz@physik.fu-berlin.de>
Adrian
--
.''`. John Paul Adrian Glaubitz
: :' : Debian Developer
`. `' Physicist
`- GPG: 62FF 8A75 84E0 2956 9546 0006 7426 3B37 F5B5 F913
^ permalink raw reply
* Re: [PATCH 0/2] apparmor unaligned memory fixes
From: John Johansen @ 2025-11-26 9:11 UTC (permalink / raw)
To: Helge Deller, Helge Deller, John Paul Adrian Glaubitz
Cc: linux-kernel, apparmor, linux-security-module, linux-parisc
In-Reply-To: <c192140a-0575-41e9-8895-6c8257ce4682@gmx.de>
On 11/25/25 13:13, Helge Deller wrote:
> On 11/25/25 20:20, John Johansen wrote:
>> On 11/25/25 07:11, Helge Deller wrote:
>>> * John Johansen <john.johansen@canonical.com>:
>>>> On 11/18/25 04:49, Helge Deller wrote:
>>>>> Hi Adrian,
>>>>>
>>>>> On 11/18/25 12:43, John Paul Adrian Glaubitz wrote:
>>>>>> On Tue, 2025-11-18 at 12:09 +0100, Helge Deller wrote:
>>>>>>> My patch fixed two call sites, but I suspect you see another call site which
>>>>>>> hasn't been fixed yet.
>>>>>>>
>>>>>>> Can you try attached patch? It might indicate the caller of the function and
>>>>>>> maybe prints the struct name/address which isn't aligned.
>>>>>>>
>>>>>>> Helge
>>>>>>>
>>>>>>>
>>>>>>> diff --git a/security/apparmor/match.c b/security/apparmor/match.c
>>>>>>> index c5a91600842a..b477430c07eb 100644
>>>>>>> --- a/security/apparmor/match.c
>>>>>>> +++ b/security/apparmor/match.c
>>>>>>> @@ -313,6 +313,9 @@ struct aa_dfa *aa_dfa_unpack(void *blob, size_t size, int flags)
>>>>>>> if (size < sizeof(struct table_set_header))
>>>>>>> goto fail;
>>>>>>> + if (WARN_ON(((unsigned long)data) & (BITS_PER_LONG/8 - 1)))
>>>>>>> + pr_warn("dfa blob stream %pS not aligned.\n", data);
>>>>>>> +
>>>>>>> if (ntohl(*(__be32 *) data) != YYTH_MAGIC)
>>>>>>> goto fail;
>>>>>>
>>>>>> Here is the relevant output with the patch applied:
>>>>>>
>>>>>> [ 73.840639] ------------[ cut here ]------------
>>>>>> [ 73.901376] WARNING: CPU: 0 PID: 341 at security/apparmor/match.c:316 aa_dfa_unpack+0x6cc/0x720
>>>>>> [ 74.015867] Modules linked in: binfmt_misc evdev flash sg drm drm_panel_orientation_quirks backlight i2c_core configfs nfnetlink autofs4 ext4 crc16 mbcache jbd2 hid_generic usbhid sr_mod hid cdrom
>>>>>> sd_mod ata_generic ohci_pci ehci_pci ehci_hcd ohci_hcd pata_ali libata sym53c8xx scsi_transport_spi tg3 scsi_mod usbcore libphy scsi_common mdio_bus usb_common
>>>>>> [ 74.428977] CPU: 0 UID: 0 PID: 341 Comm: apparmor_parser Not tainted 6.18.0-rc6+ #9 NONE
>>>>>> [ 74.536543] Call Trace:
>>>>>> [ 74.568561] [<0000000000434c24>] dump_stack+0x8/0x18
>>>>>> [ 74.633757] [<0000000000476438>] __warn+0xd8/0x100
>>>>>> [ 74.696664] [<00000000004296d4>] warn_slowpath_fmt+0x34/0x74
>>>>>> [ 74.771006] [<00000000008db28c>] aa_dfa_unpack+0x6cc/0x720
>>>>>> [ 74.843062] [<00000000008e643c>] unpack_pdb+0xbc/0x7e0
>>>>>> [ 74.910545] [<00000000008e7740>] unpack_profile+0xbe0/0x1300
>>>>>> [ 74.984888] [<00000000008e82e0>] aa_unpack+0xe0/0x6a0
>>>>>> [ 75.051226] [<00000000008e3ec4>] aa_replace_profiles+0x64/0x1160
>>>>>> [ 75.130144] [<00000000008d4d90>] policy_update+0xf0/0x280
>>>>>> [ 75.201057] [<00000000008d4fc8>] profile_replace+0xa8/0x100
>>>>>> [ 75.274258] [<0000000000766bd0>] vfs_write+0x90/0x420
>>>>>> [ 75.340594] [<00000000007670cc>] ksys_write+0x4c/0xe0
>>>>>> [ 75.406932] [<0000000000767174>] sys_write+0x14/0x40
>>>>>> [ 75.472126] [<0000000000406174>] linux_sparc_syscall+0x34/0x44
>>>>>> [ 75.548802] ---[ end trace 0000000000000000 ]---
>>>>>> [ 75.609503] dfa blob stream 0xfff0000008926b96 not aligned.
>>>>>> [ 75.682695] Kernel unaligned access at TPC[8db2a8] aa_dfa_unpack+0x6e8/0x720
>>>>>
>>>>> The non-8-byte-aligned address (0xfff0000008926b96) is coming from userspace
>>>>> (via the write syscall).
>>>>> Some apparmor userspace tool writes into the apparmor ".replace" virtual file with
>>>>> a source address which is not correctly aligned.
>>>>
>>>> the userpace buffer passed to write(2) has to be aligned? Its certainly nice if it
>>>> is but the userspace tooling hasn't been treating it as aligned. With that said,
>>>> the dfa should be padded to be aligned. So this tripping in the dfa is a bug,
>>>> and there really should be some validation to catch it.
>>>>
>>>>> You should be able to debug/find the problematic code with strace from userspace.
>>>>> Maybe someone with apparmor knowledge here on the list has an idea?
>>>>>
>>>> This is likely an unaligned 2nd profile, being split out and loaded separately
>>>> from the rest of the container. Basically the loader for some reason (there
>>>> are a few different possible reasons) is poking into the container format and
>>>> pulling out the profile at some offset, this gets loaded to the kernel but
>>>> it would seem that its causing an issue with the dfa alignment within the container,
>>>> which should be aligned to the original container.
>>>
>>>
>>> Regarding this:
>>>
>>>> Kernel side, we are going to need to add some extra verification checks, it should
>>>> be catching this, as unaligned as part of the unpack. Userspace side, we will have
>>>> to verify my guess and fix the loader.
>>>
>>> I wonder if loading those tables are really time critical?
>>
>> no, most policy is loaded once on boot and then at package upgrades. There are some
>> bits that may be loaded at application startup like, snap, libvirt, lxd, basically
>> container managers might do some thing custom per container.
>>
>> Its the run time we want to minimize, the cost of.
>>
>> Policy already can be unaligned (the container format rework to fix this is low
>> priority), and is treated as untrusted. It goes through an unpack, and translation to
>> machine native, with as many bounds checks, necessary transforms etc done at unpack
>> time as possible, so that the run time costs can be minimized.
>>
>>> If not, maybe just making the kernel aware that the tables might be unaligned
>>> can help, e.g. with the following (untested) patch.
>>> Adrian, maybe you want to test?
>>>
>>
>>> ------------------------
>>>
>>> [PATCH] Allow apparmor to handle unaligned dfa tables
>>>
>>> The dfa tables can originate from kernel or userspace and 8-byte alignment
>>> isn't always guaranteed and as such may trigger unaligned memory accesses
>>> on various architectures.
>>> Work around it by using the get_unaligned_xx() helpers.
>>>
>>> Signed-off-by: Helge Deller <deller@gmx.de>
>>>
>> lgtm,
>>
>> Acked-by: John Johansen <john.johansen@canonical.com>
>>
>> I'll pull this into my tree regardless of whether it fixes the issue
>> for Adrian, as it definitely fixes an issue.
>>
>> We can added additional patches on top s needed.
>
> My patch does not modify the UNPACK_ARRAY() macro, which we
> possibly should adjust as well.
Indeed. See the patch below. I am not surprised testing hasn't triggered this
case, but a malicious userspace could certainly construct a policy that would
trigger it. Yes it would have to be root, but I still would like to prevent
root from being able to trigger this.
> Adrian's testing seems to trigger only a few unaligned accesses,
> so maybe it's not a issue currently.
>
I don't think the userspace compiler is generating one that is bad, but it
possible to construct one and get it to the point where it can trip in
UNPACK_ARRAY
commit 2c87528c1e7be3976b61ac797c6c8700364c4c63
Author: John Johansen <john.johansen@canonical.com>
Date: Tue Nov 25 13:59:32 2025 -0800
apparmor: fix unaligned memory access of UNPACK_ARRAY
The UNPACK_ARRAY macro has the potential to have unaligned memory
access when the unpacking an unaligned profile, which is caused by
userspace splitting up a profile container before sending it to the
kernel.
While this is corner case, policy loaded from userspace should be
treated as untrusted so ensure that userspace can not trigger an
unaligned access.
Signed-off-by: John Johansen <john.johansen@canonical.com>
diff --git a/security/apparmor/include/match.h b/security/apparmor/include/match.h
index 1fbe82f5021b1..203f7c07529f5 100644
--- a/security/apparmor/include/match.h
+++ b/security/apparmor/include/match.h
@@ -104,7 +104,7 @@ struct aa_dfa {
struct table_header *tables[YYTD_ID_TSIZE];
};
-#define byte_to_byte(X) (X)
+#define byte_to_byte(X) *(X)
#define UNPACK_ARRAY(TABLE, BLOB, LEN, TTYPE, BTYPE, NTOHX) \
do { \
@@ -112,7 +112,7 @@ struct aa_dfa {
TTYPE *__t = (TTYPE *) TABLE; \
BTYPE *__b = (BTYPE *) BLOB; \
for (__i = 0; __i < LEN; __i++) { \
- __t[__i] = NTOHX(__b[__i]); \
+ __t[__i] = NTOHX(&__b[__i]); \
} \
} while (0)
diff --git a/security/apparmor/match.c b/security/apparmor/match.c
index 26e82ba879d44..3dcc342337aca 100644
--- a/security/apparmor/match.c
+++ b/security/apparmor/match.c
@@ -71,10 +71,10 @@ static struct table_header *unpack_table(char *blob, size_t bsize)
u8, u8, byte_to_byte);
else if (th.td_flags == YYTD_DATA16)
UNPACK_ARRAY(table->td_data, blob, th.td_lolen,
- u16, __be16, be16_to_cpu);
+ u16, __be16, get_unaligned_be16);
else if (th.td_flags == YYTD_DATA32)
UNPACK_ARRAY(table->td_data, blob, th.td_lolen,
- u32, __be32, be32_to_cpu);
+ u32, __be32, get_unaligned_be32);
else
goto fail;
/* if table was vmalloced make sure the page tables are synced
^ permalink raw reply related
* Re: [PATCH 0/2] apparmor unaligned memory fixes
From: david laight @ 2025-11-26 10:44 UTC (permalink / raw)
To: John Johansen
Cc: Helge Deller, Helge Deller, John Paul Adrian Glaubitz,
linux-kernel, apparmor, linux-security-module, linux-parisc
In-Reply-To: <d35010b3-7d07-488c-b5a4-a13380d0ef7c@canonical.com>
On Wed, 26 Nov 2025 01:11:45 -0800
John Johansen <john.johansen@canonical.com> wrote:
> On 11/25/25 13:13, Helge Deller wrote:
> > On 11/25/25 20:20, John Johansen wrote:
> >> On 11/25/25 07:11, Helge Deller wrote:
> >>> * John Johansen <john.johansen@canonical.com>:
> >>>> On 11/18/25 04:49, Helge Deller wrote:
> >>>>> Hi Adrian,
> >>>>>
> >>>>> On 11/18/25 12:43, John Paul Adrian Glaubitz wrote:
> >>>>>> On Tue, 2025-11-18 at 12:09 +0100, Helge Deller wrote:
> >>>>>>> My patch fixed two call sites, but I suspect you see another call site which
> >>>>>>> hasn't been fixed yet.
> >>>>>>>
> >>>>>>> Can you try attached patch? It might indicate the caller of the function and
> >>>>>>> maybe prints the struct name/address which isn't aligned.
> >>>>>>>
> >>>>>>> Helge
> >>>>>>>
> >>>>>>>
> >>>>>>> diff --git a/security/apparmor/match.c b/security/apparmor/match.c
> >>>>>>> index c5a91600842a..b477430c07eb 100644
> >>>>>>> --- a/security/apparmor/match.c
> >>>>>>> +++ b/security/apparmor/match.c
> >>>>>>> @@ -313,6 +313,9 @@ struct aa_dfa *aa_dfa_unpack(void *blob, size_t size, int flags)
> >>>>>>> if (size < sizeof(struct table_set_header))
> >>>>>>> goto fail;
> >>>>>>> + if (WARN_ON(((unsigned long)data) & (BITS_PER_LONG/8 - 1)))
> >>>>>>> + pr_warn("dfa blob stream %pS not aligned.\n", data);
> >>>>>>> +
> >>>>>>> if (ntohl(*(__be32 *) data) != YYTH_MAGIC)
> >>>>>>> goto fail;
> >>>>>>
> >>>>>> Here is the relevant output with the patch applied:
> >>>>>>
> >>>>>> [ 73.840639] ------------[ cut here ]------------
> >>>>>> [ 73.901376] WARNING: CPU: 0 PID: 341 at security/apparmor/match.c:316 aa_dfa_unpack+0x6cc/0x720
> >>>>>> [ 74.015867] Modules linked in: binfmt_misc evdev flash sg drm drm_panel_orientation_quirks backlight i2c_core configfs nfnetlink autofs4 ext4 crc16 mbcache jbd2 hid_generic usbhid sr_mod hid cdrom
> >>>>>> sd_mod ata_generic ohci_pci ehci_pci ehci_hcd ohci_hcd pata_ali libata sym53c8xx scsi_transport_spi tg3 scsi_mod usbcore libphy scsi_common mdio_bus usb_common
> >>>>>> [ 74.428977] CPU: 0 UID: 0 PID: 341 Comm: apparmor_parser Not tainted 6.18.0-rc6+ #9 NONE
> >>>>>> [ 74.536543] Call Trace:
> >>>>>> [ 74.568561] [<0000000000434c24>] dump_stack+0x8/0x18
> >>>>>> [ 74.633757] [<0000000000476438>] __warn+0xd8/0x100
> >>>>>> [ 74.696664] [<00000000004296d4>] warn_slowpath_fmt+0x34/0x74
> >>>>>> [ 74.771006] [<00000000008db28c>] aa_dfa_unpack+0x6cc/0x720
> >>>>>> [ 74.843062] [<00000000008e643c>] unpack_pdb+0xbc/0x7e0
> >>>>>> [ 74.910545] [<00000000008e7740>] unpack_profile+0xbe0/0x1300
> >>>>>> [ 74.984888] [<00000000008e82e0>] aa_unpack+0xe0/0x6a0
> >>>>>> [ 75.051226] [<00000000008e3ec4>] aa_replace_profiles+0x64/0x1160
> >>>>>> [ 75.130144] [<00000000008d4d90>] policy_update+0xf0/0x280
> >>>>>> [ 75.201057] [<00000000008d4fc8>] profile_replace+0xa8/0x100
> >>>>>> [ 75.274258] [<0000000000766bd0>] vfs_write+0x90/0x420
> >>>>>> [ 75.340594] [<00000000007670cc>] ksys_write+0x4c/0xe0
> >>>>>> [ 75.406932] [<0000000000767174>] sys_write+0x14/0x40
> >>>>>> [ 75.472126] [<0000000000406174>] linux_sparc_syscall+0x34/0x44
> >>>>>> [ 75.548802] ---[ end trace 0000000000000000 ]---
> >>>>>> [ 75.609503] dfa blob stream 0xfff0000008926b96 not aligned.
> >>>>>> [ 75.682695] Kernel unaligned access at TPC[8db2a8] aa_dfa_unpack+0x6e8/0x720
> >>>>>
> >>>>> The non-8-byte-aligned address (0xfff0000008926b96) is coming from userspace
> >>>>> (via the write syscall).
> >>>>> Some apparmor userspace tool writes into the apparmor ".replace" virtual file with
> >>>>> a source address which is not correctly aligned.
> >>>>
> >>>> the userpace buffer passed to write(2) has to be aligned? Its certainly nice if it
> >>>> is but the userspace tooling hasn't been treating it as aligned. With that said,
> >>>> the dfa should be padded to be aligned. So this tripping in the dfa is a bug,
> >>>> and there really should be some validation to catch it.
> >>>>
> >>>>> You should be able to debug/find the problematic code with strace from userspace.
> >>>>> Maybe someone with apparmor knowledge here on the list has an idea?
> >>>>>
> >>>> This is likely an unaligned 2nd profile, being split out and loaded separately
> >>>> from the rest of the container. Basically the loader for some reason (there
> >>>> are a few different possible reasons) is poking into the container format and
> >>>> pulling out the profile at some offset, this gets loaded to the kernel but
> >>>> it would seem that its causing an issue with the dfa alignment within the container,
> >>>> which should be aligned to the original container.
> >>>
> >>>
> >>> Regarding this:
> >>>
> >>>> Kernel side, we are going to need to add some extra verification checks, it should
> >>>> be catching this, as unaligned as part of the unpack. Userspace side, we will have
> >>>> to verify my guess and fix the loader.
> >>>
> >>> I wonder if loading those tables are really time critical?
> >>
> >> no, most policy is loaded once on boot and then at package upgrades. There are some
> >> bits that may be loaded at application startup like, snap, libvirt, lxd, basically
> >> container managers might do some thing custom per container.
> >>
> >> Its the run time we want to minimize, the cost of.
> >>
> >> Policy already can be unaligned (the container format rework to fix this is low
> >> priority), and is treated as untrusted. It goes through an unpack, and translation to
> >> machine native, with as many bounds checks, necessary transforms etc done at unpack
> >> time as possible, so that the run time costs can be minimized.
> >>
> >>> If not, maybe just making the kernel aware that the tables might be unaligned
> >>> can help, e.g. with the following (untested) patch.
> >>> Adrian, maybe you want to test?
> >>>
> >>
> >>> ------------------------
> >>>
> >>> [PATCH] Allow apparmor to handle unaligned dfa tables
> >>>
> >>> The dfa tables can originate from kernel or userspace and 8-byte alignment
> >>> isn't always guaranteed and as such may trigger unaligned memory accesses
> >>> on various architectures.
> >>> Work around it by using the get_unaligned_xx() helpers.
> >>>
> >>> Signed-off-by: Helge Deller <deller@gmx.de>
> >>>
> >> lgtm,
> >>
> >> Acked-by: John Johansen <john.johansen@canonical.com>
> >>
> >> I'll pull this into my tree regardless of whether it fixes the issue
> >> for Adrian, as it definitely fixes an issue.
> >>
> >> We can added additional patches on top s needed.
> >
> > My patch does not modify the UNPACK_ARRAY() macro, which we
> > possibly should adjust as well.
>
> Indeed. See the patch below. I am not surprised testing hasn't triggered this
> case, but a malicious userspace could certainly construct a policy that would
> trigger it. Yes it would have to be root, but I still would like to prevent
> root from being able to trigger this.
>
> > Adrian's testing seems to trigger only a few unaligned accesses,
> > so maybe it's not a issue currently.
> >
> I don't think the userspace compiler is generating one that is bad, but it
> possible to construct one and get it to the point where it can trip in
> UNPACK_ARRAY
>
> commit 2c87528c1e7be3976b61ac797c6c8700364c4c63
> Author: John Johansen <john.johansen@canonical.com>
> Date: Tue Nov 25 13:59:32 2025 -0800
>
> apparmor: fix unaligned memory access of UNPACK_ARRAY
>
> The UNPACK_ARRAY macro has the potential to have unaligned memory
> access when the unpacking an unaligned profile, which is caused by
> userspace splitting up a profile container before sending it to the
> kernel.
>
> While this is corner case, policy loaded from userspace should be
> treated as untrusted so ensure that userspace can not trigger an
> unaligned access.
>
> Signed-off-by: John Johansen <john.johansen@canonical.com>
>
> diff --git a/security/apparmor/include/match.h b/security/apparmor/include/match.h
> index 1fbe82f5021b1..203f7c07529f5 100644
> --- a/security/apparmor/include/match.h
> +++ b/security/apparmor/include/match.h
> @@ -104,7 +104,7 @@ struct aa_dfa {
> struct table_header *tables[YYTD_ID_TSIZE];
> };
>
> -#define byte_to_byte(X) (X)
> +#define byte_to_byte(X) *(X)
Even though is is only used once that ought to be (*(X))
>
> #define UNPACK_ARRAY(TABLE, BLOB, LEN, TTYPE, BTYPE, NTOHX) \
> do { \
> @@ -112,7 +112,7 @@ struct aa_dfa {
> TTYPE *__t = (TTYPE *) TABLE; \
> BTYPE *__b = (BTYPE *) BLOB; \
> for (__i = 0; __i < LEN; __i++) { \
> - __t[__i] = NTOHX(__b[__i]); \
> + __t[__i] = NTOHX(&__b[__i]); \
> } \
> } while (0)
>
> diff --git a/security/apparmor/match.c b/security/apparmor/match.c
> index 26e82ba879d44..3dcc342337aca 100644
> --- a/security/apparmor/match.c
> +++ b/security/apparmor/match.c
> @@ -71,10 +71,10 @@ static struct table_header *unpack_table(char *blob, size_t bsize)
> u8, u8, byte_to_byte);
Is that that just memcpy() ?
David
> else if (th.td_flags == YYTD_DATA16)
> UNPACK_ARRAY(table->td_data, blob, th.td_lolen,
> - u16, __be16, be16_to_cpu);
> + u16, __be16, get_unaligned_be16);
> else if (th.td_flags == YYTD_DATA32)
> UNPACK_ARRAY(table->td_data, blob, th.td_lolen,
> - u32, __be32, be32_to_cpu);
> + u32, __be32, get_unaligned_be32);
> else
> goto fail;
> /* if table was vmalloced make sure the page tables are synced
>
>
>
^ permalink raw reply
* Re: [PATCH 0/2] apparmor unaligned memory fixes
From: Helge Deller @ 2025-11-26 11:03 UTC (permalink / raw)
To: david laight, John Johansen
Cc: Helge Deller, John Paul Adrian Glaubitz, linux-kernel, apparmor,
linux-security-module, linux-parisc
In-Reply-To: <20251126104444.29002552@pumpkin>
On 11/26/25 11:44, david laight wrote:
> On Wed, 26 Nov 2025 01:11:45 -0800
> John Johansen <john.johansen@canonical.com> wrote:
>
>> On 11/25/25 13:13, Helge Deller wrote:
>>> On 11/25/25 20:20, John Johansen wrote:
>>>> On 11/25/25 07:11, Helge Deller wrote:
>>>>> * John Johansen <john.johansen@canonical.com>:
>>>>>> On 11/18/25 04:49, Helge Deller wrote:
>>>>>>> Hi Adrian,
>>>>>>>
>>>>>>> On 11/18/25 12:43, John Paul Adrian Glaubitz wrote:
>>>>>>>> On Tue, 2025-11-18 at 12:09 +0100, Helge Deller wrote:
>>>>>>>>> My patch fixed two call sites, but I suspect you see another call site which
>>>>>>>>> hasn't been fixed yet.
>>>>>>>>>
>>>>>>>>> Can you try attached patch? It might indicate the caller of the function and
>>>>>>>>> maybe prints the struct name/address which isn't aligned.
>>>>>>>>>
>>>>>>>>> Helge
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> diff --git a/security/apparmor/match.c b/security/apparmor/match.c
>>>>>>>>> index c5a91600842a..b477430c07eb 100644
>>>>>>>>> --- a/security/apparmor/match.c
>>>>>>>>> +++ b/security/apparmor/match.c
>>>>>>>>> @@ -313,6 +313,9 @@ struct aa_dfa *aa_dfa_unpack(void *blob, size_t size, int flags)
>>>>>>>>> if (size < sizeof(struct table_set_header))
>>>>>>>>> goto fail;
>>>>>>>>> + if (WARN_ON(((unsigned long)data) & (BITS_PER_LONG/8 - 1)))
>>>>>>>>> + pr_warn("dfa blob stream %pS not aligned.\n", data);
>>>>>>>>> +
>>>>>>>>> if (ntohl(*(__be32 *) data) != YYTH_MAGIC)
>>>>>>>>> goto fail;
>>>>>>>>
>>>>>>>> Here is the relevant output with the patch applied:
>>>>>>>>
>>>>>>>> [ 73.840639] ------------[ cut here ]------------
>>>>>>>> [ 73.901376] WARNING: CPU: 0 PID: 341 at security/apparmor/match.c:316 aa_dfa_unpack+0x6cc/0x720
>>>>>>>> [ 74.015867] Modules linked in: binfmt_misc evdev flash sg drm drm_panel_orientation_quirks backlight i2c_core configfs nfnetlink autofs4 ext4 crc16 mbcache jbd2 hid_generic usbhid sr_mod hid cdrom
>>>>>>>> sd_mod ata_generic ohci_pci ehci_pci ehci_hcd ohci_hcd pata_ali libata sym53c8xx scsi_transport_spi tg3 scsi_mod usbcore libphy scsi_common mdio_bus usb_common
>>>>>>>> [ 74.428977] CPU: 0 UID: 0 PID: 341 Comm: apparmor_parser Not tainted 6.18.0-rc6+ #9 NONE
>>>>>>>> [ 74.536543] Call Trace:
>>>>>>>> [ 74.568561] [<0000000000434c24>] dump_stack+0x8/0x18
>>>>>>>> [ 74.633757] [<0000000000476438>] __warn+0xd8/0x100
>>>>>>>> [ 74.696664] [<00000000004296d4>] warn_slowpath_fmt+0x34/0x74
>>>>>>>> [ 74.771006] [<00000000008db28c>] aa_dfa_unpack+0x6cc/0x720
>>>>>>>> [ 74.843062] [<00000000008e643c>] unpack_pdb+0xbc/0x7e0
>>>>>>>> [ 74.910545] [<00000000008e7740>] unpack_profile+0xbe0/0x1300
>>>>>>>> [ 74.984888] [<00000000008e82e0>] aa_unpack+0xe0/0x6a0
>>>>>>>> [ 75.051226] [<00000000008e3ec4>] aa_replace_profiles+0x64/0x1160
>>>>>>>> [ 75.130144] [<00000000008d4d90>] policy_update+0xf0/0x280
>>>>>>>> [ 75.201057] [<00000000008d4fc8>] profile_replace+0xa8/0x100
>>>>>>>> [ 75.274258] [<0000000000766bd0>] vfs_write+0x90/0x420
>>>>>>>> [ 75.340594] [<00000000007670cc>] ksys_write+0x4c/0xe0
>>>>>>>> [ 75.406932] [<0000000000767174>] sys_write+0x14/0x40
>>>>>>>> [ 75.472126] [<0000000000406174>] linux_sparc_syscall+0x34/0x44
>>>>>>>> [ 75.548802] ---[ end trace 0000000000000000 ]---
>>>>>>>> [ 75.609503] dfa blob stream 0xfff0000008926b96 not aligned.
>>>>>>>> [ 75.682695] Kernel unaligned access at TPC[8db2a8] aa_dfa_unpack+0x6e8/0x720
>>>>>>>
>>>>>>> The non-8-byte-aligned address (0xfff0000008926b96) is coming from userspace
>>>>>>> (via the write syscall).
>>>>>>> Some apparmor userspace tool writes into the apparmor ".replace" virtual file with
>>>>>>> a source address which is not correctly aligned.
>>>>>>
>>>>>> the userpace buffer passed to write(2) has to be aligned? Its certainly nice if it
>>>>>> is but the userspace tooling hasn't been treating it as aligned. With that said,
>>>>>> the dfa should be padded to be aligned. So this tripping in the dfa is a bug,
>>>>>> and there really should be some validation to catch it.
>>>>>>
>>>>>>> You should be able to debug/find the problematic code with strace from userspace.
>>>>>>> Maybe someone with apparmor knowledge here on the list has an idea?
>>>>>>>
>>>>>> This is likely an unaligned 2nd profile, being split out and loaded separately
>>>>>> from the rest of the container. Basically the loader for some reason (there
>>>>>> are a few different possible reasons) is poking into the container format and
>>>>>> pulling out the profile at some offset, this gets loaded to the kernel but
>>>>>> it would seem that its causing an issue with the dfa alignment within the container,
>>>>>> which should be aligned to the original container.
>>>>>
>>>>>
>>>>> Regarding this:
>>>>>
>>>>>> Kernel side, we are going to need to add some extra verification checks, it should
>>>>>> be catching this, as unaligned as part of the unpack. Userspace side, we will have
>>>>>> to verify my guess and fix the loader.
>>>>>
>>>>> I wonder if loading those tables are really time critical?
>>>>
>>>> no, most policy is loaded once on boot and then at package upgrades. There are some
>>>> bits that may be loaded at application startup like, snap, libvirt, lxd, basically
>>>> container managers might do some thing custom per container.
>>>>
>>>> Its the run time we want to minimize, the cost of.
>>>>
>>>> Policy already can be unaligned (the container format rework to fix this is low
>>>> priority), and is treated as untrusted. It goes through an unpack, and translation to
>>>> machine native, with as many bounds checks, necessary transforms etc done at unpack
>>>> time as possible, so that the run time costs can be minimized.
>>>>
>>>>> If not, maybe just making the kernel aware that the tables might be unaligned
>>>>> can help, e.g. with the following (untested) patch.
>>>>> Adrian, maybe you want to test?
>>>>>
>>>>
>>>>> ------------------------
>>>>>
>>>>> [PATCH] Allow apparmor to handle unaligned dfa tables
>>>>>
>>>>> The dfa tables can originate from kernel or userspace and 8-byte alignment
>>>>> isn't always guaranteed and as such may trigger unaligned memory accesses
>>>>> on various architectures.
>>>>> Work around it by using the get_unaligned_xx() helpers.
>>>>>
>>>>> Signed-off-by: Helge Deller <deller@gmx.de>
>>>>>
>>>> lgtm,
>>>>
>>>> Acked-by: John Johansen <john.johansen@canonical.com>
>>>>
>>>> I'll pull this into my tree regardless of whether it fixes the issue
>>>> for Adrian, as it definitely fixes an issue.
>>>>
>>>> We can added additional patches on top s needed.
>>>
>>> My patch does not modify the UNPACK_ARRAY() macro, which we
>>> possibly should adjust as well.
>>
>> Indeed. See the patch below. I am not surprised testing hasn't triggered this
>> case, but a malicious userspace could certainly construct a policy that would
>> trigger it. Yes it would have to be root, but I still would like to prevent
>> root from being able to trigger this.
>>
>>> Adrian's testing seems to trigger only a few unaligned accesses,
>>> so maybe it's not a issue currently.
>>>
>> I don't think the userspace compiler is generating one that is bad, but it
>> possible to construct one and get it to the point where it can trip in
>> UNPACK_ARRAY
>>
>> commit 2c87528c1e7be3976b61ac797c6c8700364c4c63
>> Author: John Johansen <john.johansen@canonical.com>
>> Date: Tue Nov 25 13:59:32 2025 -0800
>>
>> apparmor: fix unaligned memory access of UNPACK_ARRAY
>>
>> The UNPACK_ARRAY macro has the potential to have unaligned memory
>> access when the unpacking an unaligned profile, which is caused by
>> userspace splitting up a profile container before sending it to the
>> kernel.
>>
>> While this is corner case, policy loaded from userspace should be
>> treated as untrusted so ensure that userspace can not trigger an
>> unaligned access.
>>
>> Signed-off-by: John Johansen <john.johansen@canonical.com>
>>
>> diff --git a/security/apparmor/include/match.h b/security/apparmor/include/match.h
>> index 1fbe82f5021b1..203f7c07529f5 100644
>> --- a/security/apparmor/include/match.h
>> +++ b/security/apparmor/include/match.h
>> @@ -104,7 +104,7 @@ struct aa_dfa {
>> struct table_header *tables[YYTD_ID_TSIZE];
>> };
>>
>> -#define byte_to_byte(X) (X)
>> +#define byte_to_byte(X) *(X)
>
> Even though is is only used once that ought to be (*(X))
>
>>
>> #define UNPACK_ARRAY(TABLE, BLOB, LEN, TTYPE, BTYPE, NTOHX) \
>> do { \
>> @@ -112,7 +112,7 @@ struct aa_dfa {
>> TTYPE *__t = (TTYPE *) TABLE; \
>> BTYPE *__b = (BTYPE *) BLOB; \
>> for (__i = 0; __i < LEN; __i++) { \
>> - __t[__i] = NTOHX(__b[__i]); \
>> + __t[__i] = NTOHX(&__b[__i]); \
>> } \
>> } while (0)
>>
>> diff --git a/security/apparmor/match.c b/security/apparmor/match.c
>> index 26e82ba879d44..3dcc342337aca 100644
>> --- a/security/apparmor/match.c
>> +++ b/security/apparmor/match.c
>> @@ -71,10 +71,10 @@ static struct table_header *unpack_table(char *blob, size_t bsize)
>> u8, u8, byte_to_byte);
>
> Is that that just memcpy() ?
No, it's memcpy() only on big-endian machines.
On little-endian machines it converts from big-endian
16/32-bit ints to little-endian 16/32-bit ints.
But I see some potential for optimization here:
a) on big-endian machines just use memcpy()
b) on little-endian machines use memcpy() to copy from possibly-unaligned
memory to then known-to-be-aligned destination. Then use a loop with
be32_to_cpu() instead of get_unaligned_xx() as it's faster.
Thoughts?
Helge
> David
>
>> else if (th.td_flags == YYTD_DATA16)
>> UNPACK_ARRAY(table->td_data, blob, th.td_lolen,
>> - u16, __be16, be16_to_cpu);
>> + u16, __be16, get_unaligned_be16);
>> else if (th.td_flags == YYTD_DATA32)
>> UNPACK_ARRAY(table->td_data, blob, th.td_lolen,
>> - u32, __be32, be32_to_cpu);
>> + u32, __be32, get_unaligned_be32);
>> else
>> goto fail;
>> /* if table was vmalloced make sure the page tables are synced
>>
>>
>>
>
^ permalink raw reply
* Re: [PATCH 0/2] apparmor unaligned memory fixes
From: Helge Deller @ 2025-11-26 11:31 UTC (permalink / raw)
To: John Johansen, david laight
Cc: John Paul Adrian Glaubitz, linux-kernel, apparmor,
linux-security-module, linux-parisc
In-Reply-To: <4034ad19-8e09-440c-a042-a66a488c048b@gmx.de>
* Helge Deller <deller@gmx.de>:
> On 11/26/25 11:44, david laight wrote:
> > On Wed, 26 Nov 2025 01:11:45 -0800
> > John Johansen <john.johansen@canonical.com> wrote:
> >
> > > On 11/25/25 13:13, Helge Deller wrote:
> > > > On 11/25/25 20:20, John Johansen wrote:
> > > > > On 11/25/25 07:11, Helge Deller wrote:
> > > > > > * John Johansen <john.johansen@canonical.com>:
> > > > > > > On 11/18/25 04:49, Helge Deller wrote:
> > > > > > > > Hi Adrian,
> > > > > > > >
> > > > > > > > On 11/18/25 12:43, John Paul Adrian Glaubitz wrote:
> > > > > > > > > On Tue, 2025-11-18 at 12:09 +0100, Helge Deller wrote:
> > > > > > > > > > My patch fixed two call sites, but I suspect you see another call site which
> > > > > > > > > > hasn't been fixed yet.
> > > > > > > > > >
> > > > > > > > > > Can you try attached patch? It might indicate the caller of the function and
> > > > > > > > > > maybe prints the struct name/address which isn't aligned.
> > > > > > > > > >
> > > > > > > > > > Helge
> > > > > > > > > >
> > > > > > > > > >
> > > > > > > > > > diff --git a/security/apparmor/match.c b/security/apparmor/match.c
> > > > > > > > > > index c5a91600842a..b477430c07eb 100644
> > > > > > > > > > --- a/security/apparmor/match.c
> > > > > > > > > > +++ b/security/apparmor/match.c
> > > > > > > > > > @@ -313,6 +313,9 @@ struct aa_dfa *aa_dfa_unpack(void *blob, size_t size, int flags)
> > > > > > > > > > if (size < sizeof(struct table_set_header))
> > > > > > > > > > goto fail;
> > > > > > > > > > + if (WARN_ON(((unsigned long)data) & (BITS_PER_LONG/8 - 1)))
> > > > > > > > > > + pr_warn("dfa blob stream %pS not aligned.\n", data);
> > > > > > > > > > +
> > > > > > > > > > if (ntohl(*(__be32 *) data) != YYTH_MAGIC)
> > > > > > > > > > goto fail;
> > > > > > > > >
> > > > > > > > > Here is the relevant output with the patch applied:
> > > > > > > > >
> > > > > > > > > [ 73.840639] ------------[ cut here ]------------
> > > > > > > > > [ 73.901376] WARNING: CPU: 0 PID: 341 at security/apparmor/match.c:316 aa_dfa_unpack+0x6cc/0x720
> > > > > > > > > [ 74.015867] Modules linked in: binfmt_misc evdev flash sg drm drm_panel_orientation_quirks backlight i2c_core configfs nfnetlink autofs4 ext4 crc16 mbcache jbd2 hid_generic usbhid sr_mod hid cdrom
> > > > > > > > > sd_mod ata_generic ohci_pci ehci_pci ehci_hcd ohci_hcd pata_ali libata sym53c8xx scsi_transport_spi tg3 scsi_mod usbcore libphy scsi_common mdio_bus usb_common
> > > > > > > > > [ 74.428977] CPU: 0 UID: 0 PID: 341 Comm: apparmor_parser Not tainted 6.18.0-rc6+ #9 NONE
> > > > > > > > > [ 74.536543] Call Trace:
> > > > > > > > > [ 74.568561] [<0000000000434c24>] dump_stack+0x8/0x18
> > > > > > > > > [ 74.633757] [<0000000000476438>] __warn+0xd8/0x100
> > > > > > > > > [ 74.696664] [<00000000004296d4>] warn_slowpath_fmt+0x34/0x74
> > > > > > > > > [ 74.771006] [<00000000008db28c>] aa_dfa_unpack+0x6cc/0x720
> > > > > > > > > [ 74.843062] [<00000000008e643c>] unpack_pdb+0xbc/0x7e0
> > > > > > > > > [ 74.910545] [<00000000008e7740>] unpack_profile+0xbe0/0x1300
> > > > > > > > > [ 74.984888] [<00000000008e82e0>] aa_unpack+0xe0/0x6a0
> > > > > > > > > [ 75.051226] [<00000000008e3ec4>] aa_replace_profiles+0x64/0x1160
> > > > > > > > > [ 75.130144] [<00000000008d4d90>] policy_update+0xf0/0x280
> > > > > > > > > [ 75.201057] [<00000000008d4fc8>] profile_replace+0xa8/0x100
> > > > > > > > > [ 75.274258] [<0000000000766bd0>] vfs_write+0x90/0x420
> > > > > > > > > [ 75.340594] [<00000000007670cc>] ksys_write+0x4c/0xe0
> > > > > > > > > [ 75.406932] [<0000000000767174>] sys_write+0x14/0x40
> > > > > > > > > [ 75.472126] [<0000000000406174>] linux_sparc_syscall+0x34/0x44
> > > > > > > > > [ 75.548802] ---[ end trace 0000000000000000 ]---
> > > > > > > > > [ 75.609503] dfa blob stream 0xfff0000008926b96 not aligned.
> > > > > > > > > [ 75.682695] Kernel unaligned access at TPC[8db2a8] aa_dfa_unpack+0x6e8/0x720
> > > > > > > >
> > > > > > > > The non-8-byte-aligned address (0xfff0000008926b96) is coming from userspace
> > > > > > > > (via the write syscall).
> > > > > > > > Some apparmor userspace tool writes into the apparmor ".replace" virtual file with
> > > > > > > > a source address which is not correctly aligned.
> > > > > > >
> > > > > > > the userpace buffer passed to write(2) has to be aligned? Its certainly nice if it
> > > > > > > is but the userspace tooling hasn't been treating it as aligned. With that said,
> > > > > > > the dfa should be padded to be aligned. So this tripping in the dfa is a bug,
> > > > > > > and there really should be some validation to catch it.
> > > > > > > > You should be able to debug/find the problematic code with strace from userspace.
> > > > > > > > Maybe someone with apparmor knowledge here on the list has an idea?
> > > > > > > This is likely an unaligned 2nd profile, being split out and loaded separately
> > > > > > > from the rest of the container. Basically the loader for some reason (there
> > > > > > > are a few different possible reasons) is poking into the container format and
> > > > > > > pulling out the profile at some offset, this gets loaded to the kernel but
> > > > > > > it would seem that its causing an issue with the dfa alignment within the container,
> > > > > > > which should be aligned to the original container.
> > > > > >
> > > > > >
> > > > > > Regarding this:
> > > > > > > Kernel side, we are going to need to add some extra verification checks, it should
> > > > > > > be catching this, as unaligned as part of the unpack. Userspace side, we will have
> > > > > > > to verify my guess and fix the loader.
> > > > > >
> > > > > > I wonder if loading those tables are really time critical?
> > > > >
> > > > > no, most policy is loaded once on boot and then at package upgrades. There are some
> > > > > bits that may be loaded at application startup like, snap, libvirt, lxd, basically
> > > > > container managers might do some thing custom per container.
> > > > >
> > > > > Its the run time we want to minimize, the cost of.
> > > > >
> > > > > Policy already can be unaligned (the container format rework to fix this is low
> > > > > priority), and is treated as untrusted. It goes through an unpack, and translation to
> > > > > machine native, with as many bounds checks, necessary transforms etc done at unpack
> > > > > time as possible, so that the run time costs can be minimized.
> > > > > > If not, maybe just making the kernel aware that the tables might be unaligned
> > > > > > can help, e.g. with the following (untested) patch.
> > > > > > Adrian, maybe you want to test?
> > > > > > ------------------------
> > > > > >
> > > > > > [PATCH] Allow apparmor to handle unaligned dfa tables
> > > > > >
> > > > > > The dfa tables can originate from kernel or userspace and 8-byte alignment
> > > > > > isn't always guaranteed and as such may trigger unaligned memory accesses
> > > > > > on various architectures.
> > > > > > Work around it by using the get_unaligned_xx() helpers.
> > > > > >
> > > > > > Signed-off-by: Helge Deller <deller@gmx.de>
> > > > > lgtm,
> > > > >
> > > > > Acked-by: John Johansen <john.johansen@canonical.com>
> > > > >
> > > > > I'll pull this into my tree regardless of whether it fixes the issue
> > > > > for Adrian, as it definitely fixes an issue.
> > > > >
> > > > > We can added additional patches on top s needed.
> > > >
> > > > My patch does not modify the UNPACK_ARRAY() macro, which we
> > > > possibly should adjust as well.
> > >
> > > Indeed. See the patch below. I am not surprised testing hasn't triggered this
> > > case, but a malicious userspace could certainly construct a policy that would
> > > trigger it. Yes it would have to be root, but I still would like to prevent
> > > root from being able to trigger this.
> > >
> > > > Adrian's testing seems to trigger only a few unaligned accesses,
> > > > so maybe it's not a issue currently.
> > > I don't think the userspace compiler is generating one that is bad, but it
> > > possible to construct one and get it to the point where it can trip in
> > > UNPACK_ARRAY
> > >
> > > commit 2c87528c1e7be3976b61ac797c6c8700364c4c63
> > > Author: John Johansen <john.johansen@canonical.com>
> > > Date: Tue Nov 25 13:59:32 2025 -0800
> > >
> > > apparmor: fix unaligned memory access of UNPACK_ARRAY
> > > The UNPACK_ARRAY macro has the potential to have unaligned memory
> > > access when the unpacking an unaligned profile, which is caused by
> > > userspace splitting up a profile container before sending it to the
> > > kernel.
> > > While this is corner case, policy loaded from userspace should be
> > > treated as untrusted so ensure that userspace can not trigger an
> > > unaligned access.
> > > Signed-off-by: John Johansen <john.johansen@canonical.com>
> > >
> > > diff --git a/security/apparmor/include/match.h b/security/apparmor/include/match.h
> > > index 1fbe82f5021b1..203f7c07529f5 100644
> > > --- a/security/apparmor/include/match.h
> > > +++ b/security/apparmor/include/match.h
> > > @@ -104,7 +104,7 @@ struct aa_dfa {
> > > struct table_header *tables[YYTD_ID_TSIZE];
> > > };
> > > -#define byte_to_byte(X) (X)
> > > +#define byte_to_byte(X) *(X)
> >
> > Even though is is only used once that ought to be (*(X))
> >
> > > #define UNPACK_ARRAY(TABLE, BLOB, LEN, TTYPE, BTYPE, NTOHX) \
> > > do { \
> > > @@ -112,7 +112,7 @@ struct aa_dfa {
> > > TTYPE *__t = (TTYPE *) TABLE; \
> > > BTYPE *__b = (BTYPE *) BLOB; \
> > > for (__i = 0; __i < LEN; __i++) { \
> > > - __t[__i] = NTOHX(__b[__i]); \
> > > + __t[__i] = NTOHX(&__b[__i]); \
> > > } \
> > > } while (0)
> > > diff --git a/security/apparmor/match.c b/security/apparmor/match.c
> > > index 26e82ba879d44..3dcc342337aca 100644
> > > --- a/security/apparmor/match.c
> > > +++ b/security/apparmor/match.c
> > > @@ -71,10 +71,10 @@ static struct table_header *unpack_table(char *blob, size_t bsize)
> > > u8, u8, byte_to_byte);
> >
> > Is that that just memcpy() ?
>
> No, it's memcpy() only on big-endian machines.
> On little-endian machines it converts from big-endian
> 16/32-bit ints to little-endian 16/32-bit ints.
>
> But I see some potential for optimization here:
> a) on big-endian machines just use memcpy()
> b) on little-endian machines use memcpy() to copy from possibly-unaligned
> memory to then known-to-be-aligned destination. Then use a loop with
> be32_to_cpu() instead of get_unaligned_xx() as it's faster.
>
> Thoughts?
Like this (untested!) patch:
[PATCH] apparmor: Optimize table creation from possibly unaligned memory
Source blob may come from userspace and might be unaligned.
Try to optize the copying process by avoiding unaligned memory accesses.
Signed-off-by: Helge Deller <deller@gmx.de>
diff --git a/security/apparmor/include/match.h b/security/apparmor/include/match.h
index 1fbe82f5021b..225df6495c84 100644
--- a/security/apparmor/include/match.h
+++ b/security/apparmor/include/match.h
@@ -111,9 +111,14 @@ struct aa_dfa {
typeof(LEN) __i; \
TTYPE *__t = (TTYPE *) TABLE; \
BTYPE *__b = (BTYPE *) BLOB; \
- for (__i = 0; __i < LEN; __i++) { \
- __t[__i] = NTOHX(__b[__i]); \
- } \
+ BUILD_BUG_ON(sizeof(TTYPE) != sizeof(BTYPE)); \
+ /* copy to naturally aligned table address */ \
+ memcpy(__t, __b, (LEN) * sizeof(BTYPE)); \
+ /* convert from big-endian if necessary */ \
+ if (!IS_ENABLED(CONFIG_CPU_BIG_ENDIAN)) \
+ for (__i = 0; __i < LEN; __i++, __t++) { \
+ *__t = NTOHX(*__t); \
+ } \
} while (0)
static inline size_t table_size(size_t len, size_t el_size)
^ permalink raw reply related
* [PATCH v3 0/5] Implement LANDLOCK_ADD_RULE_NO_INHERIT
From: Justin Suess @ 2025-11-26 12:20 UTC (permalink / raw)
To: linux-security-module
Cc: Tingmao Wang, Günther Noack, Jan Kara, Abhinav Saxena,
Mickaël Salaün, Justin Suess
Hi,
This is version 3 of the LANDLOCK_ADD_RULE_NO_INHERIT series, which
implements a new flag to suppress inheritance of access rights and
flags from parent objects.
This series is rebased on v5 of Tingmao Wang's "quiet flag" series.
The new flag enables policies where a parent directory needs broader
access than its children. For example, a sandbox may permit read-write
access to /home/user but still prohibit writes to ~/.bashrc or
~/.ssh, even though they are nested beneath the parent. Today this is
not possible because access rights always propagate from parent to
child within a layer.
When a rule is added with LANDLOCK_ADD_RULE_NO_INHERIT:
* access rights on parent inodes are ignored for that inode and its
descendants; and
* operations that change the direct parent subtree of such objects
(rename, rmdir, link) are denied up to the mountpoint; and
* parent flags do not propagate below a NO_INHERIT rule (new in v3).
These parent-directory restrictions help mitigate sandbox-restart
attacks: a sandboxed process could otherwise move a protected
directory before exit, causing the next sandbox instance to apply its
policy to the wrong path.
Changes since v2:
1. Add six new selftests for the new flag.
2. Add an optimization to stop permission harvesting when all
relevant layers are tagged with NO_INHERIT.
3. Suppress inheritance of parent flags.
4. Rebase onto v5 of the quiet-flag series.
5. Remove the xarray structure used for flag tracking in favor of
blank rule insertion, simplifying the implementation.
6. Fix edge cases involving flag inheritance across multiple
NO_INHERIT layers.
7. Add documenting comments to new functions.
Links:
v1:
https://lore.kernel.org/linux-security-module/20251105180019.1432367-1-utilityemal77@gmail.com/T/#t
v2:
https://lore.kernel.org/linux-security-module/20251120222346.1157004-1-utilityemal77@gmail.com/T/#t
quiet-flag v5:
https://lore.kernel.org/linux-security-module/cover.1763931318.git.m@maowtm.org/T/#t
Example usage:
# LL_FS_RO="" LL_FS_RW="/" LL_FS_RO_NO_INHERIT="/a/b/c" landlock-sandboxer sh
# touch /a/b/c/fi # denied; / RW does not inherit
# rmdir /a/b/c # denied due to parent-directory protections
# mv /a /bad # denied
# mkdir /a/good; touch /a/good/fi # allowed; unrelated path
If preferred, I'm happy to split the selftests into multiple commits.
I am particularly interested in feedback on:
* The soundness of inserting blank rules in ensure_rule_for_dentry.
A zero-access rule is lazily inserted into parent directories on
first access to enforce topology-change protections. This replaces
the prior xarray tracking, and should reduce complexity and improve
performance.
* Additional edge cases that should be covered by new tests.
* Performance implications of the current design.
All existing Landlock selftests and KUnit tests, as well as the new
tests added in this series, are passing.
Thank you for your time and review.
Regards,
Justin Suess
Justin Suess (5):
landlock: Implement LANDLOCK_ADD_RULE_NO_INHERIT
landlock: Implement LANDLOCK_ADD_RULE_NO_INHERIT userspace api
samples/landlock: Add LANDLOCK_ADD_RULE_NO_INHERIT to
landlock-sandboxer
selftests/landlock: Implement selftests for
LANDLOCK_ADD_RULE_NO_INHERIT
landlock: Implement KUnit test for LANDLOCK_ADD_RULE_NO_INHERIT
include/uapi/linux/landlock.h | 29 +
samples/landlock/sandboxer.c | 37 +-
security/landlock/audit.c | 4 +-
security/landlock/domain.c | 4 +-
security/landlock/fs.c | 592 ++++++++++++++++++++-
security/landlock/ruleset.c | 116 +++-
security/landlock/ruleset.h | 36 +-
security/landlock/syscalls.c | 14 +-
tools/testing/selftests/landlock/fs_test.c | 459 +++++++++++++++-
9 files changed, 1249 insertions(+), 42 deletions(-)
base-commit: 91d200c5385c926c8d1f2df33a8a4160924fa977
--
2.51.0
^ permalink raw reply
* [PATCH v3 1/5] landlock: Implement LANDLOCK_ADD_RULE_NO_INHERIT
From: Justin Suess @ 2025-11-26 12:20 UTC (permalink / raw)
To: linux-security-module
Cc: Tingmao Wang, Günther Noack, Jan Kara, Abhinav Saxena,
Mickaël Salaün, Justin Suess
In-Reply-To: <20251126122039.3832162-1-utilityemal77@gmail.com>
Implements a flag to prevent access grant inheritance within the filesystem hierarchy
for landlock rules.
If a landlock rule on an inode has this flag, any access grants on parent inodes will be
ignored. Moreover, operations that involve altering the direct parent tree of the subject with
LANDLOCK_ADD_RULE_NO_INHERIT will be denied up to the mountpoint.
Additionally (new in v3) parent flag inheritance is blocked by this flag, allowing fine
grained access control over LANDLOCK_ADD_RULE_QUIET.
For example, if /a/b/c/ = read only + LANDLOCK_ADD_RULE_NO_INHERIT and / = read write, writes to
files in /a/b/c will be denied. Moreover, moving /a to /bad, removing /a/b/c, or creating links to
/a will be prohibited.
And if / has LANDLOCK_ADD_RULE_QUIET, /a/b/c will still audit (handled)
accesses. This is because LANDLOCK_ADD_RULE_NO_INHERIT also
suppresses flag inheritance from parent objects.
The parent directory restrictions mitigate sandbox-restart attacks. For example, if a sandboxed program
is able to move a LANDLOCK_ADD_RULE_NO_INHERIT restricted directory, upon sandbox restart, the policy
applied naively on the same filenames would be invalid. Preventing these operations mitigates these attacks.
v2..v3 changes:
* Parent directory topology protections now work by lazily
inserting blank rules on parent inodes if they do not
exist. This replaces the previous xarray implementation
with simplified logic.
* Added an optimization to skip further processing if all layers collected
no inherit.
* Added support to block flag inheritance.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
security/landlock/audit.c | 4 +-
security/landlock/domain.c | 4 +-
security/landlock/fs.c | 592 +++++++++++++++++++++++++++++++++++-
security/landlock/ruleset.c | 27 +-
security/landlock/ruleset.h | 36 ++-
5 files changed, 645 insertions(+), 18 deletions(-)
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index d51563712325..4da97dd6985c 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -588,7 +588,9 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
subject->domain, &missing, request->layer_masks,
request->layer_masks_size);
object_quiet_flag = !!(request->rule_flags.quiet_masks &
- BIT(youngest_layer));
+ BIT(youngest_layer)) &&
+ !(request->rule_flags.blocked_flag_masks &
+ BIT(youngest_layer));
} else {
youngest_layer = get_layer_from_deny_masks(
&missing, request->all_existing_optional_access,
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 8caf07250328..5bd83865c87d 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -236,7 +236,9 @@ optional_access_t landlock_get_quiet_optional_accesses(
BITS_PER_TYPE(access_mask_t)) {
const u8 layer = (deny_masks >> (access_index * 4)) &
(LANDLOCK_MAX_NUM_LAYERS - 1);
- const bool is_quiet = !!(rule_flags.quiet_masks & BIT(layer));
+ const layer_mask_t layer_bit = BIT(layer);
+ const bool is_quiet = !!(rule_flags.quiet_masks & layer_bit) &&
+ !(rule_flags.blocked_flag_masks & layer_bit);
if (is_quiet)
quiet_optional_accesses |= BIT(access_index);
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 29f10da32141..0a5c73f18f26 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -317,6 +317,206 @@ static struct landlock_object *get_inode_object(struct inode *const inode)
LANDLOCK_ACCESS_FS_IOCTL_DEV)
/* clang-format on */
+static const struct landlock_rule *find_rule(const struct landlock_ruleset *const domain,
+ const struct dentry *const dentry);
+
+/**
+ * landlock_domain_layers_mask - Build a mask covering all layers of a domain
+ * @domain: The ruleset (domain) to inspect.
+ *
+ * Return a layer mask with a 1 bit for each existing layer of @domain.
+ * If @domain has no layers 0 is returned. If the number of layers is
+ * greater than or equal to the number of bits in layer_mask_t, all bits
+ * are set.
+ */
+static layer_mask_t landlock_domain_layers_mask(const struct landlock_ruleset
+ *const domain)
+{
+ if (!domain || !domain->num_layers)
+ return 0;
+
+ if (domain->num_layers >= sizeof(layer_mask_t) * BITS_PER_BYTE)
+ return (layer_mask_t)~0ULL;
+
+ return GENMASK_ULL(domain->num_layers - 1, 0);
+}
+
+/**
+ * rule_blocks_all_layers_no_inherit - check whether a rule disables inheritance
+ * @domain_layers_mask: Mask describing the domain's active layers.
+ * @rule: Rule to inspect.
+ *
+ * Return true if every layer present in @rule has its no_inherit flag set
+ * and the set of layers covered by the rule equals @domain_layers_mask.
+ * This indicates that the rule prevents inheritance on all layers of the
+ * domain and thus further walking for inheritance checks can stop.
+ */
+static bool rule_blocks_all_layers_no_inherit(const layer_mask_t domain_layers_mask,
+ const struct landlock_rule *const rule)
+{
+ layer_mask_t rule_layers = 0;
+ u32 layer_index;
+
+ if (!domain_layers_mask || !rule)
+ return false;
+
+ for (layer_index = 0; layer_index < rule->num_layers; layer_index++) {
+ const struct landlock_layer *const layer =
+ &rule->layers[layer_index];
+ const layer_mask_t layer_bit = BIT_ULL(layer->level - 1);
+
+ if (!layer->flags.no_inherit)
+ return false;
+
+ rule_layers |= layer_bit;
+ }
+
+ return rule_layers && rule_layers == domain_layers_mask;
+}
+
+/**
+ * landlock_collect_no_inherit_layers - Collects layers with no_inherit flags
+ */
+/**
+ * landlock_collect_no_inherit_layers - collect effective no_inherit layers
+ * @ruleset: Ruleset to consult.
+ * @dentry: Dentry used as a starting point for the upward walk.
+ *
+ * Walk upwards from @dentry and return a layer mask containing the layers
+ * for which either a rule on the visited dentry has the no_inherit flag set
+ * or where an ancestor was previously marked as having a descendant with
+ * a no_inherit rule. The search prefers the closest matching dentry and
+ * stops once any relevant layer bits are found or the root is reached.
+ *
+ * Returns a layer_mask_t where each set bit corresponds to a layer with an
+ * effective no_inherit influence for @dentry. Returns 0 if none apply or if
+ * inputs are invalid.
+ */
+static layer_mask_t landlock_collect_no_inherit_layers(const struct landlock_ruleset
+ *const ruleset,
+ struct dentry *const dentry)
+{
+ struct dentry *cursor, *parent;
+ layer_mask_t layers = 0;
+ bool include_descendants = true;
+
+ if (!ruleset || !dentry || d_is_negative(dentry))
+ return 0;
+
+ cursor = dget(dentry);
+ while (true) {
+ const struct landlock_rule *rule;
+ u32 layer_index;
+
+ rule = find_rule(ruleset, cursor);
+ if (rule) {
+ for (layer_index = 0; layer_index < rule->num_layers; layer_index++) {
+ const struct landlock_layer *layer = &rule->layers[layer_index];
+
+ if (layer->flags.no_inherit ||
+ (include_descendants &&
+ layer->flags.has_no_inherit_descendant))
+ layers |= BIT_ULL((layer->level ?
+ layer->level : layer_index + 1) - 1);
+ }
+ }
+
+ if (layers) {
+ dput(cursor);
+ return layers;
+ }
+
+ if (IS_ROOT(cursor)) {
+ dput(cursor);
+ break;
+ }
+
+ parent = dget_parent(cursor);
+ dput(cursor);
+ if (!parent)
+ break;
+
+ cursor = parent;
+ include_descendants = false;
+ }
+ return 0;
+}
+
+static int mark_no_inherit_ancestors(struct landlock_ruleset *ruleset,
+ struct dentry *dentry,
+ layer_mask_t descendant_layers);
+
+/**
+ * mask_no_inherit_descendant_layers - apply descendant no_inherit masking
+ * @domain: The ruleset (domain) to consult.
+ * @dentry: The dentry whose descendants are considered.
+ * @child_layers: Layers present on the child that may be subject to masking.
+ * @access_request: Accesses being requested (bitmask).
+ * @layer_masks: Per-access layer masks to be modified in-place.
+ * @rule_flags: Collected flags which will be updated accordingly.
+ *
+ * If descendant dentries have no_inherit, clear that
+ * layer's bit from @layer_masks. Also updates @rule_flags to reflect
+ * which layers were blocked. Returns true if any of the @layer_masks were
+ * modified, false otherwise.
+ */
+static bool mask_no_inherit_descendant_layers(const struct landlock_ruleset
+ *const domain,
+ struct dentry *const dentry,
+ layer_mask_t child_layers,
+ const access_mask_t access_request,
+ layer_mask_t
+ (*const layer_masks)
+ [LANDLOCK_NUM_ACCESS_FS],
+ struct collected_rule_flags
+ *const rule_flags)
+{
+ layer_mask_t descendant_layers;
+ const unsigned long access_req = access_request;
+ unsigned long access_bit;
+ bool changed = false;
+
+ if (!access_request || !layer_masks || !rule_flags || !dentry)
+ return false;
+ if (d_is_negative(dentry))
+ return false;
+
+ descendant_layers = landlock_collect_no_inherit_layers(domain, dentry);
+ {
+ layer_mask_t shared_layers = descendant_layers & child_layers;
+
+ if (shared_layers) {
+ rule_flags->no_inherit_masks |= shared_layers;
+ rule_flags->no_inherit_desc_masks |= shared_layers;
+ rule_flags->blocked_flag_masks |= shared_layers;
+ }
+ }
+ descendant_layers &= ~child_layers;
+ descendant_layers &= ~rule_flags->no_inherit_masks;
+ if (!descendant_layers)
+ return false;
+
+ rule_flags->blocked_flag_masks |= descendant_layers;
+
+ for_each_set_bit(access_bit, &access_req,
+ ARRAY_SIZE(*layer_masks)) {
+ layer_mask_t *const layer_mask = &(*layer_masks)[access_bit];
+
+ if (*layer_mask & descendant_layers) {
+ *layer_mask &= ~descendant_layers;
+ changed = true;
+ }
+ }
+
+ if (!changed)
+ return false;
+
+ rule_flags->no_inherit_masks |= descendant_layers;
+ rule_flags->no_inherit_desc_masks |= descendant_layers;
+
+ return true;
+}
+
/*
* @path: Should have been checked by get_path_from_fd().
*/
@@ -325,12 +525,13 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
access_mask_t access_rights, const int flags)
{
int err;
+ const bool is_dir = d_is_dir(path->dentry);
struct landlock_id id = {
.type = LANDLOCK_KEY_INODE,
};
/* Files only get access rights that make sense. */
- if (!d_is_dir(path->dentry) &&
+ if (!is_dir &&
(access_rights | ACCESS_FILE) != ACCESS_FILE)
return -EINVAL;
if (WARN_ON_ONCE(ruleset->num_layers != 1))
@@ -344,13 +545,43 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
return PTR_ERR(id.key.object);
mutex_lock(&ruleset->lock);
err = landlock_insert_rule(ruleset, id, access_rights, flags);
+ if (!err && (flags & LANDLOCK_ADD_RULE_NO_INHERIT)) {
+ const struct landlock_rule *rule;
+ layer_mask_t descendant_layers = 0;
+ u32 layer_index;
+
+ rule = find_rule(ruleset, path->dentry);
+ if (rule) {
+ for (layer_index = 0; layer_index < rule->num_layers; layer_index++) {
+ const struct landlock_layer *layer =
+ &rule->layers[layer_index];
+
+ if (layer->flags.no_inherit ||
+ layer->flags.has_no_inherit_descendant)
+ descendant_layers |=
+ BIT_ULL((layer->level ?
+ layer->level : layer_index + 1) - 1);
+ }
+ if (descendant_layers) {
+ err = mark_no_inherit_ancestors(ruleset, path->dentry,
+ descendant_layers);
+ if (err)
+ goto out_unlock;
+ }
+ }
+ }
mutex_unlock(&ruleset->lock);
+out:
/*
* No need to check for an error because landlock_insert_rule()
* increments the refcount for the new object if needed.
*/
landlock_put_object(id.key.object);
return err;
+
+out_unlock:
+ mutex_unlock(&ruleset->lock);
+ goto out;
}
/* Access-control management */
@@ -382,6 +613,134 @@ find_rule(const struct landlock_ruleset *const domain,
return rule;
}
+/**
+ * ensure_rule_for_dentry - ensure a ruleset contains a rule entry for dentry,
+ * inserting a blank rule if needed.
+ * @ruleset: Ruleset to modify/inspect. Caller must hold @ruleset->lock.
+ * @dentry: Dentry to ensure a rule exists for.
+ *
+ * If no rule is currently associated with @dentry, insert an empty rule
+ * (with zero access) tied to the backing inode. Returns a pointer to the
+ * rule associated with @dentry on success, NULL when @dentry is negative, or
+ * an ERR_PTR()-encoded error if the rule cannot be created.
+ *
+ * This is useful for LANDLOCK_ADD_RULE_NO_INHERIT processing, where a rule
+ * may need to be created for an ancestor dentry that does not yet have one
+ * to properly track no_inherit flags.
+ *
+ * The flags are set to zero if a rule is newly created, and the caller
+ * is responsible for setting them appropriately.
+ *
+ * The returned rule pointer's lifetime is tied to @ruleset.
+ */
+static const struct landlock_rule *
+ensure_rule_for_dentry(struct landlock_ruleset *const ruleset,
+ struct dentry *const dentry)
+{
+ struct landlock_id id = {
+ .type = LANDLOCK_KEY_INODE,
+ };
+ const struct landlock_rule *rule;
+ int err;
+
+ if (!ruleset || !dentry || d_is_negative(dentry))
+ return NULL;
+
+ lockdep_assert_held(&ruleset->lock);
+
+ rule = find_rule(ruleset, dentry);
+ if (rule)
+ return rule;
+
+ id.key.object = get_inode_object(d_backing_inode(dentry));
+ if (IS_ERR(id.key.object))
+ return ERR_CAST(id.key.object);
+
+ err = landlock_insert_rule(ruleset, id, 0, 0);
+ landlock_put_object(id.key.object);
+ if (err)
+ return ERR_PTR(err);
+
+ rule = find_rule(ruleset, dentry);
+ return rule ? rule : ERR_PTR(-ENOENT);
+}
+
+/**
+ * mark_no_inherit_ancestors - mark ancestors as having no_inherit descendants
+ * @ruleset: Ruleset to modify. Caller must hold @ruleset->lock.
+ * @dentry: Dentry representing the descendant that carries no_inherit bits.
+ * @descendant_layers: Mask of layers from the descendant that should be
+ * advertised to ancestors via has_no_inherit_descendant.
+ *
+ * Walks upward from @dentry and ensures that any ancestor rule contains the
+ * has_no_inherit_descendant marker for the specified @descendant_layers so
+ * parent lookups can quickly detect descendant no_inherit influence.
+ *
+ * Returns 0 on success or a negative errno if ancestor bookkeeping fails.
+ */
+static int mark_no_inherit_ancestors(struct landlock_ruleset *ruleset,
+ struct dentry *dentry,
+ layer_mask_t descendant_layers)
+{
+ struct dentry *cursor;
+ u32 layer_index;
+ int err = 0;
+
+ if (!ruleset || !dentry || !descendant_layers)
+ return -EINVAL;
+
+ lockdep_assert_held(&ruleset->lock);
+
+ cursor = dget(dentry);
+ while (cursor) {
+ struct dentry *parent;
+
+ if (IS_ROOT(cursor)) {
+ dput(cursor);
+ break;
+ }
+
+ parent = dget_parent(cursor);
+ dput(cursor);
+ if (!parent)
+ break;
+
+ if (!d_is_negative(parent)) {
+ const struct landlock_rule *rule;
+ /* Ensures a rule exists for the parent dentry,
+ * inserting a blank one if needed
+ */
+ rule = ensure_rule_for_dentry(ruleset, parent);
+ if (IS_ERR(rule)) {
+ err = PTR_ERR(rule);
+ dput(parent);
+ cursor = NULL;
+ break;
+ }
+ if (rule) {
+ struct landlock_rule *mutable_rule =
+ (struct landlock_rule *)rule;
+
+ for (layer_index = 0;
+ layer_index < mutable_rule->num_layers;
+ layer_index++) {
+ struct landlock_layer *layer =
+ &mutable_rule->layers[layer_index];
+ layer_mask_t layer_bit =
+ BIT_ULL((layer->level ?
+ layer->level : layer_index + 1) - 1);
+
+ if (descendant_layers & layer_bit)
+ layer->flags.has_no_inherit_descendant = true;
+ }
+ }
+ }
+
+ cursor = parent;
+ }
+ return err;
+}
+
/*
* Allows access to pseudo filesystems that will never be mountable (e.g.
* sockfs, pipefs), but can still be reachable through
@@ -764,6 +1123,8 @@ static bool is_access_to_paths_allowed(
struct landlock_request *const log_request_parent2,
struct dentry *const dentry_child2)
{
+ const layer_mask_t domain_layers_mask =
+ landlock_domain_layers_mask(domain);
bool allowed_parent1 = false, allowed_parent2 = false, is_dom_check,
is_dom_check_bkp, child1_is_directory = true,
child2_is_directory = true;
@@ -778,6 +1139,13 @@ static bool is_access_to_paths_allowed(
struct collected_rule_flags *rule_flags_parent1 = &log_request_parent1->rule_flags;
struct collected_rule_flags *rule_flags_parent2 = &log_request_parent2->rule_flags;
struct collected_rule_flags _rule_flag_parent1_bkp, _rule_flag_parent2_bkp;
+ layer_mask_t child1_layers = 0;
+ layer_mask_t child2_layers = 0;
+
+ if (dentry_child1)
+ child1_layers = landlock_collect_no_inherit_layers(domain, dentry_child1);
+ if (dentry_child2)
+ child2_layers = landlock_collect_no_inherit_layers(domain, dentry_child2);
if (!access_request_parent1 && !access_request_parent2)
return true;
@@ -931,6 +1299,10 @@ static bool is_access_to_paths_allowed(
ARRAY_SIZE(*layer_masks_parent2),
rule_flags_parent2);
+ if (rule &&
+ rule_blocks_all_layers_no_inherit(domain_layers_mask, rule))
+ break;
+
/* Stops when a rule from each layer grants access. */
if (allowed_parent1 && allowed_parent2) {
/*
@@ -976,8 +1348,13 @@ static bool is_access_to_paths_allowed(
memcpy(&_rule_flag_parent2_bkp,
rule_flags_parent2,
sizeof(_rule_flag_parent2_bkp));
- is_dom_check_bkp = is_dom_check;
}
+ is_dom_check_bkp = is_dom_check;
+ child1_layers = landlock_collect_no_inherit_layers(domain,
+ walker_path
+ .dentry);
+ if (layer_masks_parent2)
+ child2_layers = child1_layers;
/* Ignores hidden mount points. */
goto jump_up;
@@ -1001,15 +1378,50 @@ static bool is_access_to_paths_allowed(
break;
}
- /*
- * We reached a disconnected root directory from a bind mount, and
- * we need to reset the walk to the current mount root.
- */
- goto reset_to_mount_root;
- }
- parent_dentry = dget_parent(walker_path.dentry);
- dput(walker_path.dentry);
- walker_path.dentry = parent_dentry;
+ /*
+ * We reached a disconnected root directory from a bind mount, and
+ * we need to reset the walk to the current mount root.
+ */
+ goto reset_to_mount_root;
+ }
+ if (likely(!d_is_negative(walker_path.dentry))) {
+ child1_layers = landlock_collect_no_inherit_layers(domain,
+ walker_path.dentry);
+ if (layer_masks_parent2)
+ child2_layers = child1_layers;
+ } else {
+ child1_layers = 0;
+ if (layer_masks_parent2)
+ child2_layers = 0;
+ }
+ parent_dentry = dget_parent(walker_path.dentry);
+ dput(walker_path.dentry);
+ walker_path.dentry = parent_dentry;
+ /*
+ * Apply descendant no-inherit masking now that we've moved to the
+ * parent. This ensures the parent respects any no-inherit rules from
+ * the child we just left. Only applies to refer operations (rename/link).
+ */
+ if (unlikely(layer_masks_parent2)) {
+ if (mask_no_inherit_descendant_layers(domain, walker_path.dentry,
+ child1_layers,
+ access_masked_parent1,
+ layer_masks_parent1,
+ rule_flags_parent1))
+ allowed_parent1 =
+ allowed_parent1 ||
+ is_layer_masks_allowed(layer_masks_parent1);
+
+ if (rule_flags_parent2 &&
+ mask_no_inherit_descendant_layers(domain, walker_path.dentry,
+ child2_layers,
+ access_masked_parent2,
+ layer_masks_parent2,
+ rule_flags_parent2))
+ allowed_parent2 =
+ allowed_parent2 ||
+ is_layer_masks_allowed(layer_masks_parent2);
+ }
continue;
reset_to_mount_root:
@@ -1057,6 +1469,10 @@ static bool is_access_to_paths_allowed(
dput(walker_path.dentry);
walker_path.dentry = walker_path.mnt->mnt_root;
dget(walker_path.dentry);
+ child1_layers = landlock_collect_no_inherit_layers(domain,
+ walker_path.dentry);
+ if (layer_masks_parent2)
+ child2_layers = child1_layers;
}
path_put(&walker_path);
@@ -1172,6 +1588,8 @@ static bool collect_domain_accesses(
struct collected_rule_flags *const rule_flags)
{
access_mask_t access_dom;
+ const layer_mask_t domain_layers_mask =
+ landlock_domain_layers_mask(domain);
bool ret = false;
if (WARN_ON_ONCE(!domain || !mnt_dir || !dir || !layer_masks_dom))
@@ -1187,9 +1605,11 @@ static bool collect_domain_accesses(
while (true) {
struct dentry *parent_dentry;
+ const struct landlock_rule *rule = find_rule(domain, dir);
+
/* Gets all layers allowing all domain accesses. */
if (landlock_unmask_layers(
- find_rule(domain, dir), access_dom, layer_masks_dom,
+ rule, access_dom, layer_masks_dom,
ARRAY_SIZE(*layer_masks_dom), rule_flags)) {
/*
* Before allowing this side of the access request, checks that the
@@ -1206,6 +1626,10 @@ static bool collect_domain_accesses(
break;
}
+ if (rule &&
+ rule_blocks_all_layers_no_inherit(domain_layers_mask, rule))
+ break;
+
/* Stops at the mount point. */
if (dir == mnt_dir->dentry)
break;
@@ -1232,6 +1656,121 @@ static bool collect_domain_accesses(
return ret;
}
+/**
+ * collect_topology_sealed_layers - collect layers sealed against topology changes
+ * @domain: Ruleset to consult.
+ * @dentry: Starting dentry for the upward walk.
+ * @override_layers: Optional out parameter filled with layers that are
+ * present on ancestors but considered overrides (not
+ * sealing the topology for descendants).
+ *
+ * Walk upwards from @dentry and return a mask of layers where either the
+ * visited dentry contains a no_inherit rule or ancestors were previously
+ * marked as having a descendant with no_inherit. @override_layers, if not
+ * NULL, is filled with layers that would normally be overridden by more
+ * specific descendant rules.
+ *
+ * Returns a layer mask where set bits indicate layers that are "sealed"
+ * (topology changes like rename/rmdir are denied) for the subtree rooted at
+ * @dentry.
+ *
+ * Useful for LANDLOCK_ADD_RULE_NO_INHERIT parent directory enforcement to ensure
+ * that topology changes do not violate the no_inherit constraints.
+ */
+static layer_mask_t
+collect_topology_sealed_layers(const struct landlock_ruleset *const domain,
+ struct dentry *dentry,
+ layer_mask_t *const override_layers)
+{
+ struct dentry *cursor, *parent;
+ bool include_descendants = true;
+ layer_mask_t sealed_layers = 0;
+
+ if (override_layers)
+ *override_layers = 0;
+
+ if (!domain || !dentry || d_is_negative(dentry))
+ return 0;
+
+ cursor = dget(dentry);
+ while (cursor) {
+ const struct landlock_rule *rule;
+ u32 layer_index;
+
+ rule = find_rule(domain, cursor);
+ if (rule) {
+ for (layer_index = 0; layer_index < rule->num_layers;
+ layer_index++) {
+ const struct landlock_layer *layer =
+ &rule->layers[layer_index];
+ const int level = layer->level ? layer->level :
+ layer_index + 1;
+ layer_mask_t layer_bit = BIT_ULL(level - 1);
+
+ if (include_descendants &&
+ (layer->flags.no_inherit ||
+ layer->flags.has_no_inherit_descendant)) {
+ sealed_layers |= layer_bit;
+ } else if (override_layers) {
+ *override_layers |= layer_bit;
+ }
+ }
+ }
+
+ if (sealed_layers || IS_ROOT(cursor))
+ break;
+
+ parent = dget_parent(cursor);
+ dput(cursor);
+ if (!parent)
+ return sealed_layers;
+
+ cursor = parent;
+ include_descendants = false;
+ }
+ dput(cursor);
+ return sealed_layers;
+}
+
+/**
+ * deny_no_inherit_topology_change - deny topology changes on sealed layers
+ * @subject: Subject performing the operation (contains the domain).
+ * @dentry: Dentry that is the target of the topology modification.
+ *
+ * Checks whether any domain layers are sealed against topology changes at
+ * @dentry (via collect_topology_sealed_layers). If so, emit an audit record
+ * and return -EACCES. Otherwise return 0.
+ */
+static int deny_no_inherit_topology_change(const struct landlock_cred_security
+ *subject,
+ struct dentry *dentry)
+{
+ layer_mask_t sealed_layers;
+ layer_mask_t override_layers;
+ unsigned long layer_index;
+
+ if (!subject || !dentry || d_is_negative(dentry))
+ return 0;
+ sealed_layers = collect_topology_sealed_layers(subject->domain,
+ dentry, &override_layers);
+ sealed_layers &= ~override_layers;
+
+ if (!sealed_layers)
+ return 0;
+
+ layer_index = __ffs((unsigned long)sealed_layers);
+ landlock_log_denial(subject, &(struct landlock_request) {
+ .type = LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY,
+ .audit = {
+ .type = LSM_AUDIT_DATA_DENTRY,
+ .u.dentry = dentry,
+ },
+ .layer_plus_one = layer_index + 1,
+ });
+
+ return -EACCES;
+}
+
/**
* current_check_refer_path - Check if a rename or link action is allowed
*
@@ -1316,6 +1855,16 @@ static int current_check_refer_path(struct dentry *const old_dentry,
access_request_parent2 =
get_mode_access(d_backing_inode(old_dentry)->i_mode);
if (removable) {
+ int err;
+
+ err = deny_no_inherit_topology_change(subject, old_dentry);
+ if (err)
+ return err;
+ if (exchange) {
+ err = deny_no_inherit_topology_change(subject, new_dentry);
+ if (err)
+ return err;
+ }
access_request_parent1 |= maybe_remove(old_dentry);
access_request_parent2 |= maybe_remove(new_dentry);
}
@@ -1707,12 +2256,31 @@ static int hook_path_symlink(const struct path *const dir,
static int hook_path_unlink(const struct path *const dir,
struct dentry *const dentry)
{
+ const struct landlock_cred_security *const subject =
+ landlock_get_applicable_subject(current_cred(), any_fs, NULL);
+ int err;
+
+ if (subject) {
+ err = deny_no_inherit_topology_change(subject, dentry);
+ if (err)
+ return err;
+ }
return current_check_access_path(dir, LANDLOCK_ACCESS_FS_REMOVE_FILE);
}
static int hook_path_rmdir(const struct path *const dir,
struct dentry *const dentry)
{
+ const struct landlock_cred_security *const subject =
+ landlock_get_applicable_subject(current_cred(), any_fs, NULL);
+ int err;
+
+ if (subject) {
+ err = deny_no_inherit_topology_change(subject, dentry);
+ if (err)
+ return err;
+ }
+
return current_check_access_path(dir, LANDLOCK_ACCESS_FS_REMOVE_DIR);
}
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 750a444e1983..f7b6a48bbf39 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -255,8 +255,13 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
return -EINVAL;
if (WARN_ON_ONCE(this->layers[0].level != 0))
return -EINVAL;
+ /* Merge the flags into the rules */
this->layers[0].access |= (*layers)[0].access;
this->layers[0].flags.quiet |= (*layers)[0].flags.quiet;
+ this->layers[0].flags.no_inherit |=
+ (*layers)[0].flags.no_inherit;
+ this->layers[0].flags.has_no_inherit_descendant |=
+ (*layers)[0].flags.has_no_inherit_descendant;
return 0;
}
@@ -315,7 +320,10 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
.level = 0,
.flags = {
.quiet = !!(flags & LANDLOCK_ADD_RULE_QUIET),
- },
+ .no_inherit = !!(flags & LANDLOCK_ADD_RULE_NO_INHERIT),
+ .has_no_inherit_descendant =
+ !!(flags & LANDLOCK_ADD_RULE_NO_INHERIT),
+ }
} };
build_check_layer();
@@ -662,9 +670,22 @@ bool landlock_unmask_layers(const struct landlock_rule *const rule,
unsigned long access_bit;
bool is_empty;
- /* Collect rule flags for each layer. */
- if (rule_flags && layer->flags.quiet)
+ /* Skip layers that already have no inherit flags. */
+ if (rule_flags &&
+ (rule_flags->no_inherit_masks & layer_bit))
+ continue;
+
+ /* Collect rule flags for each layer.
+ * We block flag inheritance if needed
+ * because of a no_inherit rule.
+ */
+ if (rule_flags && layer->flags.quiet &&
+ !(rule_flags->blocked_flag_masks & layer_bit))
rule_flags->quiet_masks |= layer_bit;
+ if (rule_flags && layer->flags.no_inherit)
+ rule_flags->no_inherit_masks |= layer_bit;
+ if (rule_flags && layer->flags.has_no_inherit_descendant)
+ rule_flags->no_inherit_desc_masks |= layer_bit;
/*
* Records in @layer_masks which layer grants access to each requested
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index eb60db646422..8b46ab14e995 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -40,6 +40,21 @@ struct landlock_layer {
* down the file hierarchy.
*/
bool quiet:1;
+ /**
+ * @no_inherit: Prevents this rule from being inherited by
+ * descendant directories in the filesystem layer. Only used
+ * for filesystem rules.
+ */
+ bool no_inherit:1;
+ /**
+ * @has_no_inherit_descendant: Marker to indicate that this layer
+ * has at least one descendant directory with a rule having the
+ * no_inherit flag. Only used for filesystem rules.
+ * This "flag" is not set by the user, but by Landlock on
+ * parent directories of rules when the child rule has
+ * a rule with the no_inherit flag.
+ */
+ bool has_no_inherit_descendant:1;
} flags;
/**
* @access: Bitfield of allowed actions on the kernel object. They are
@@ -49,13 +64,32 @@ struct landlock_layer {
};
/**
- * struct collected_rule_flags - Hold accumulated flags for each layer.
+ * struct collected_rule_flags - Hold accumulated flags and their markers for each layer.
*/
struct collected_rule_flags {
/**
* @quiet_masks: Layers for which the quiet flag is effective.
*/
layer_mask_t quiet_masks;
+ /**
+ * @no_inherit_masks: Layers for which the no_inherit flag is effective.
+ */
+ layer_mask_t no_inherit_masks;
+ /**
+ * @no_inherit_desc_masks: Layers for which the
+ * has_no_inherit_descendant tag is effective.
+ * This is not a flag itself, but a marker set on ancestors
+ * of rules with the no_inherit flag to deny topology changes
+ * in the direct parent path.
+ */
+ layer_mask_t no_inherit_desc_masks;
+ /**
+ * @blocked_flag_masks: Layers where flag inheritance must be blocked
+ * because of a no_inherit rule. This is not a flag itself, but a marker
+ * for layers that have their flags blocked due to no_inherit rule
+ * propagation.
+ */
+ layer_mask_t blocked_flag_masks;
};
/**
--
2.51.0
^ permalink raw reply related
* [PATCH v3 2/5] landlock: Implement LANDLOCK_ADD_RULE_NO_INHERIT userspace api
From: Justin Suess @ 2025-11-26 12:20 UTC (permalink / raw)
To: linux-security-module
Cc: Tingmao Wang, Günther Noack, Jan Kara, Abhinav Saxena,
Mickaël Salaün, Justin Suess
In-Reply-To: <20251126122039.3832162-1-utilityemal77@gmail.com>
Implements the syscall side flag handling and kernel api headers for the
LANDLOCK_ADD_RULE_NO_INHERIT flag.
v2..v3 changes:
* Extended documentation for flag inheritance suppression on
LANDLOCK_ADD_RULE_NO_INHERIT.
* Extended the flag validation rules in the syscall.
* Added mention of no inherit in empty rules in add_rule_path_beneath
as per Tingmao Wang's suggestion.
* Added check for useless no-inherit flag in networking rules.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
include/uapi/linux/landlock.h | 29 +++++++++++++++++++++++++++++
security/landlock/syscalls.c | 14 +++++++++++---
2 files changed, 40 insertions(+), 3 deletions(-)
diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index d4f47d20361a..cf5c8068f513 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -127,10 +127,39 @@ struct landlock_ruleset_attr {
* 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.
+ * %LANDLOCK_ADD_RULE_NO_INHERIT
+ * When set on a rule being added to a ruleset, this flag disables the
+ * inheritance of access rights and flags from parent objects.
+ *
+ * This flag currently applies only to filesystem rules. Adding it to
+ * non-filesystem rules wil return -EINVAL, unless future extensions
+ * of Landlock define other hierarchical object types.
+ *
+ * By default, Landlock filesystem rules inherit allowed accesses from
+ * ancestor directories: if a parent directory grants certain rights,
+ * those rights also apply to its children. A rule marked with
+ * LANDLOCK_ADD_RULE_NO_INHERIT stops this propagation at the directory
+ * covered by the rule. Descendants of that directory continue to inherit
+ * normally unless they also have rules using this flag.
+ *
+ * If a regular file is marked with this flag, it will not inherit any
+ * access rights from its parent directories; only the accesses explicitly
+ * allowed by the rule will apply to that file.
+ *
+ * This flag also enforces parent-directory restrictions: rename, rmdir,
+ * link, and other operations that would change the directory's immediate
+ * parent subtree are denied up to the mount point. This prevents
+ * sandboxed processes from manipulating the filesystem hierarchy to evade
+ * restrictions (e.g., via sandbox-restart attacks).
+ *
+ * In addition, this flag blocks the inheritance of rule-layer flags
+ * (such as the quiet flag) from parent directories to the object covered
+ * by this rule.
*/
/* clang-format off */
#define LANDLOCK_ADD_RULE_QUIET (1U << 0)
+#define LANDLOCK_ADD_RULE_NO_INHERIT (1U << 1)
/* clang-format on */
/**
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 93396bfc1500..1ea9bf95ef61 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -352,7 +352,7 @@ 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. However, the rule is not useless if it
- * is there to hold a quiet flag
+ * is there to hold a quiet or no inherit flag.
*/
if (!flags && !path_beneath_attr.allowed_access)
return -ENOMSG;
@@ -407,6 +407,10 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
if (flags & LANDLOCK_ADD_RULE_QUIET && !ruleset->quiet_masks.net)
return -EINVAL;
+ /* No inherit is always useless for this scope */
+ if (flags & LANDLOCK_ADD_RULE_NO_INHERIT)
+ return -EINVAL;
+
/* Denies inserting a rule with port greater than 65535. */
if (net_port_attr.port > U16_MAX)
return -EINVAL;
@@ -462,8 +466,12 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
if (!is_initialized())
return -EOPNOTSUPP;
-
- if (flags && flags != LANDLOCK_ADD_RULE_QUIET)
+ /* Checks flag existence */
+ if (flags && flags & ~(LANDLOCK_ADD_RULE_QUIET | LANDLOCK_ADD_RULE_NO_INHERIT))
+ return -EINVAL;
+ /* No inherit may only apply on path_beneath rules. */
+ if ((flags & LANDLOCK_ADD_RULE_NO_INHERIT) &&
+ rule_type != LANDLOCK_RULE_PATH_BENEATH)
return -EINVAL;
/* Gets and checks the ruleset. */
--
2.51.0
^ permalink raw reply related
* [PATCH v3 3/5] samples/landlock: Add LANDLOCK_ADD_RULE_NO_INHERIT to landlock-sandboxer
From: Justin Suess @ 2025-11-26 12:20 UTC (permalink / raw)
To: linux-security-module
Cc: Tingmao Wang, Günther Noack, Jan Kara, Abhinav Saxena,
Mickaël Salaün, Justin Suess
In-Reply-To: <20251126122039.3832162-1-utilityemal77@gmail.com>
Adds support to landlock-sandboxer with environment variables LL_FS_RO_NO_INHERIT
and LL_FS_RW_NO_INHERIT. These create the same rulesets as their non-no inherit variants,
plus the LANDLOCK_ADD_RULE_NO_INHERIT flag.
v2..v3 changes:
* Minor formatting fixes
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
samples/landlock/sandboxer.c | 37 +++++++++++++++++++++++++++---------
1 file changed, 28 insertions(+), 9 deletions(-)
diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
index 2d8e3e94b77b..6f6bfc4e5110 100644
--- a/samples/landlock/sandboxer.c
+++ b/samples/landlock/sandboxer.c
@@ -58,6 +58,8 @@ 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_RO_NO_INHERIT_NAME "LL_FS_RO_NO_INHERIT"
+#define ENV_FS_RW_NO_INHERIT_NAME "LL_FS_RW_NO_INHERIT"
#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"
@@ -121,7 +123,8 @@ 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, bool quiet)
+ const __u64 allowed_access,
+ __u32 add_rule_flags, bool mandatory)
{
int num_paths, i, ret = 1;
char *env_path_name;
@@ -132,9 +135,13 @@ static int populate_ruleset_fs(const char *const env_var, const int ruleset_fd,
env_path_name = getenv(env_var);
if (!env_path_name) {
- /* Prevents users to forget a setting. */
- fprintf(stderr, "Missing environment variable %s\n", env_var);
- return 1;
+ if (mandatory) {
+ /* Prevents from forgetting to set necessary env vars. */
+ fprintf(stderr, "Missing environment variable %s\n",
+ env_var);
+ return 1;
+ }
+ return 0;
}
env_path_name = strdup(env_path_name);
unsetenv(env_var);
@@ -171,8 +178,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,
- quiet ? LANDLOCK_ADD_RULE_QUIET : 0)) {
+ &path_beneath, add_rule_flags)) {
fprintf(stderr,
"Failed to update the ruleset with \"%s\": %s\n",
path_list[i], strerror(errno));
@@ -375,6 +381,8 @@ static const char help[] =
"Optional settings (when not set, their associated access check "
"is always allowed, which is different from an empty string which "
"means an empty list):\n"
+ "* " ENV_FS_RO_NO_INHERIT_NAME ": read-only paths without rule inheritance\n"
+ "* " ENV_FS_RW_NO_INHERIT_NAME ": read-write paths without rule inheritance\n"
"* " ENV_TCP_BIND_NAME ": ports allowed to bind (server)\n"
"* " ENV_TCP_CONNECT_NAME ": ports allowed to connect (client)\n"
"* " ENV_SCOPED_NAME ": actions denied on the outside of the landlock domain\n"
@@ -596,17 +604,28 @@ int main(const int argc, char *const argv[], char *const *const envp)
}
if (populate_ruleset_fs(ENV_FS_RO_NAME, ruleset_fd, access_fs_ro,
- false)) {
+ 0, true)) {
goto err_close_ruleset;
}
if (populate_ruleset_fs(ENV_FS_RW_NAME, ruleset_fd, access_fs_rw,
+ 0, true)) {
+ goto err_close_ruleset;
+ }
+ /* Optional no-inherit rules mirror the regular read-only/read-write sets. */
+ if (populate_ruleset_fs(ENV_FS_RO_NO_INHERIT_NAME, ruleset_fd,
+ access_fs_ro, LANDLOCK_ADD_RULE_NO_INHERIT,
+ false)) {
+ goto err_close_ruleset;
+ }
+ if (populate_ruleset_fs(ENV_FS_RW_NO_INHERIT_NAME, ruleset_fd,
+ access_fs_rw, LANDLOCK_ADD_RULE_NO_INHERIT,
false)) {
goto err_close_ruleset;
}
/* Don't require this env to be present. */
- if (quiet_supported && getenv(ENV_FS_QUIET_NAME)) {
+ if (quiet_supported) {
if (populate_ruleset_fs(ENV_FS_QUIET_NAME, ruleset_fd, 0,
- true)) {
+ LANDLOCK_ADD_RULE_QUIET, false)) {
goto err_close_ruleset;
}
}
--
2.51.0
^ permalink raw reply related
* [PATCH v3 4/5] selftests/landlock: Implement selftests for LANDLOCK_ADD_RULE_NO_INHERIT
From: Justin Suess @ 2025-11-26 12:20 UTC (permalink / raw)
To: linux-security-module
Cc: Tingmao Wang, Günther Noack, Jan Kara, Abhinav Saxena,
Mickaël Salaün, Justin Suess
In-Reply-To: <20251126122039.3832162-1-utilityemal77@gmail.com>
Implements 11 selftests for the flag, covering allowed and disallowed operations on parent
and child directories when this flag is set, as well as multi-layer configurations
and flag inheritance / audit logging.
v2..v3 changes:
* Also covers flag inheritance, audit logging and LANDLOCK_ADD_RULE_QUIET suppression.
* Increases number of selftests from 5 -> 11.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
tools/testing/selftests/landlock/fs_test.c | 459 ++++++++++++++++++++-
1 file changed, 447 insertions(+), 12 deletions(-)
diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index 6aa65d344c72..87b66ad7a0b8 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -717,16 +717,12 @@ 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, bool quiet)
+ const int ruleset_fd, const __u64 allowed_access,
+ const char *const path, __u32 flags)
{
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = allowed_access,
};
- __u32 flags = 0;
-
- if (quiet)
- flags |= LANDLOCK_ADD_RULE_QUIET;
path_beneath.parent_fd = open(path, O_PATH | O_CLOEXEC);
ASSERT_LE(0, path_beneath.parent_fd)
@@ -790,7 +786,7 @@ static int create_ruleset(struct __test_metadata *const _metadata,
continue;
add_path_beneath(_metadata, ruleset_fd, rules[i].access,
- rules[i].path, false);
+ rules[i].path, 0);
}
return ruleset_fd;
}
@@ -1368,7 +1364,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, false);
+ dir_s1d2, 0);
/*
* According to ruleset_fd, dir_s1d2 should now have the
* LANDLOCK_ACCESS_FS_READ_FILE and LANDLOCK_ACCESS_FS_WRITE_FILE
@@ -1400,7 +1396,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, false);
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RW, dir_s1d1, 0);
enforce_ruleset(_metadata, ruleset_fd);
/* Same tests and results as above. */
@@ -1423,7 +1419,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, false);
+ dir_s1d3, 0);
enforce_ruleset(_metadata, ruleset_fd);
ASSERT_EQ(0, close(ruleset_fd));
@@ -1476,7 +1472,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, false);
+ dir_s1d2, 0);
enforce_ruleset(_metadata, ruleset_fd);
ASSERT_EQ(0, close(ruleset_fd));
@@ -1488,6 +1484,111 @@ TEST_F_FORK(layout1, inherit_superset)
ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY));
}
+TEST_F_FORK(layout1, inherit_no_inherit_flag)
+{
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = ACCESS_RW,
+ };
+ int ruleset_fd;
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RW, dir_s1d1, 0);
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d2,
+ LANDLOCK_ADD_RULE_NO_INHERIT);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ /* Parent directory still grants write access to its direct children. */
+ EXPECT_EQ(0, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY));
+ EXPECT_EQ(0, test_open(file1_s1d1, O_WRONLY));
+
+ /* dir_s1d2 gets only its explicit read-only access rights. */
+ EXPECT_EQ(0, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY));
+ EXPECT_EQ(0, test_open(file1_s1d2, O_RDONLY));
+ EXPECT_EQ(EACCES, test_open(file1_s1d2, O_WRONLY));
+
+ /* Descendants of dir_s1d2 inherit the reduced access mask. */
+ EXPECT_EQ(0, test_open(dir_s1d3, O_RDONLY | O_DIRECTORY));
+ EXPECT_EQ(0, test_open(file1_s1d3, O_RDONLY));
+ EXPECT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY));
+}
+
+TEST_F_FORK(layout1, inherit_no_inherit_nested_levels)
+{
+ int ruleset_fd;
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = ACCESS_RW | LANDLOCK_ACCESS_FS_REFER |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE |
+ LANDLOCK_ACCESS_FS_REMOVE_DIR,
+ };
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ /* Level 1: s1d1 (RW + REFER + REMOVE + NO_INHERIT) */
+ add_path_beneath(_metadata, ruleset_fd,
+ ACCESS_RW | LANDLOCK_ACCESS_FS_REFER |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE |
+ LANDLOCK_ACCESS_FS_REMOVE_DIR,
+ dir_s1d1, LANDLOCK_ADD_RULE_NO_INHERIT);
+
+ /* Level 2: s1d2 (RO + NO_INHERIT) */
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d2,
+ LANDLOCK_ADD_RULE_NO_INHERIT);
+
+ /* Level 3: s1d3 (RW + REFER + REMOVE + NO_INHERIT) */
+ add_path_beneath(_metadata, ruleset_fd,
+ ACCESS_RW | LANDLOCK_ACCESS_FS_REFER |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE |
+ LANDLOCK_ACCESS_FS_REMOVE_DIR,
+ dir_s1d3, LANDLOCK_ADD_RULE_NO_INHERIT);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ /*
+ * Level 3: s1d3
+ * - RW allowed (unlink file)
+ * - REFER allowed (rename file)
+ * - REMOVE_DIR denied (parent s1d2 is part of direct parent tree)
+ */
+ ASSERT_EQ(0, unlink(file1_s1d3));
+ ASSERT_EQ(0, rename(file2_s1d3, file1_s1d3));
+ ASSERT_EQ(0, rename(file1_s1d3, file2_s1d3));
+ ASSERT_EQ(-1, rmdir(dir_s1d3));
+ ASSERT_EQ(EACCES, errno);
+
+ /*
+ * Level 2: s1d2
+ * - RW denied (unlink file), layer is RO
+ * - REFER denied (rename file)
+ * - REMOVE_DIR of s1d2 not allowed (parent s1d1 is part of direct parent tree)
+ */
+ ASSERT_EQ(-1, unlink(file1_s1d2));
+ ASSERT_EQ(EACCES, errno);
+ ASSERT_EQ(-1, rename(file2_s1d2, file1_s1d2));
+ ASSERT_EQ(EACCES, errno);
+ ASSERT_EQ(-1, rmdir(dir_s1d2));
+ ASSERT_EQ(EACCES, errno);
+
+ /*
+ * Level 1: s1d1
+ * - RW allowed
+ * - Rename allowed (except for direct parent tree s1d2)
+ * - REMOVE_DIR denied (parent tmp is denied)
+ */
+ ASSERT_EQ(0, unlink(file1_s1d1));
+ ASSERT_EQ(0, rename(file2_s1d1, file1_s1d1));
+ ASSERT_EQ(0, rename(file1_s1d1, file2_s1d1));
+ ASSERT_EQ(-1, rmdir(dir_s1d1));
+ ASSERT_EQ(EACCES, errno);
+}
+
TEST_F_FORK(layout0, max_layers)
{
int i, err;
@@ -4412,6 +4513,246 @@ TEST_F_FORK(layout1, named_unix_domain_socket_ioctl)
ASSERT_EQ(0, close(cli_fd));
}
+TEST_F_FORK(layout1, inherit_no_inherit_topology_dir)
+{
+ const struct rule rules[] = {
+ {
+ .path = TMP_DIR,
+ .access = ACCESS_RW | LANDLOCK_ACCESS_FS_REMOVE_FILE,
+ },
+ {},
+ };
+ int ruleset_fd;
+
+ ruleset_fd = create_ruleset(_metadata,
+ ACCESS_RW | LANDLOCK_ACCESS_FS_REMOVE_FILE,
+ rules);
+ ASSERT_LE(0, ruleset_fd);
+
+ /* Adds a no-inherit rule on a leaf directory. */
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d3,
+ LANDLOCK_ADD_RULE_NO_INHERIT);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ /*
+ * Topology modifications of the rule path and its parents are denied.
+ */
+
+ /* Target directory s1d3 */
+ ASSERT_EQ(-1, rmdir(dir_s1d3));
+ ASSERT_EQ(EACCES, errno);
+ ASSERT_EQ(-1, rename(dir_s1d3, dir_s2d3));
+ ASSERT_EQ(EACCES, errno);
+
+ /* Parent directory s1d2 */
+ ASSERT_EQ(-1, rmdir(dir_s1d2));
+ ASSERT_EQ(EACCES, errno);
+ ASSERT_EQ(-1, rename(dir_s1d2, dir_s2d2));
+ ASSERT_EQ(EACCES, errno);
+
+ /* Grandparent directory s1d1 */
+ ASSERT_EQ(-1, rmdir(dir_s1d1));
+ ASSERT_EQ(EACCES, errno);
+ ASSERT_EQ(-1, rename(dir_s1d1, dir_s2d1));
+ ASSERT_EQ(EACCES, errno);
+
+ /*
+ * Sibling operations are allowed.
+ */
+ /* Sibling of s1d3 */
+ ASSERT_EQ(0, unlink(file1_s1d2));
+ /* Sibling of s1d2 */
+ ASSERT_EQ(0, unlink(file1_s1d1));
+
+ /*
+ * Content of the no-inherit directory is restricted by the rule (RO).
+ */
+ ASSERT_EQ(-1, unlink(file1_s1d3));
+ ASSERT_EQ(EACCES, errno);
+}
+
+TEST_F_FORK(layout1, no_inherit_allow_inner_removal)
+{
+ int ruleset_fd;
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = ACCESS_RW | LANDLOCK_ACCESS_FS_REMOVE_FILE,
+ };
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ add_path_beneath(_metadata, ruleset_fd,
+ ACCESS_RW | LANDLOCK_ACCESS_FS_REMOVE_FILE, dir_s1d2,
+ LANDLOCK_ADD_RULE_NO_INHERIT);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ /*
+ * Content of the no-inherit directory is mutable (RW).
+ * This checks that the no-inherit flag does not seal the content.
+ */
+ ASSERT_EQ(0, unlink(file1_s1d2));
+
+ /*
+ * Topology modifications of the rule path are denied.
+ */
+ ASSERT_EQ(-1, rmdir(dir_s1d2));
+ ASSERT_EQ(EACCES, errno);
+ ASSERT_EQ(-1, rename(dir_s1d2, dir_s2d2));
+ ASSERT_EQ(EACCES, errno);
+}
+
+TEST_F_FORK(layout1, inherit_no_inherit_topology_unrelated)
+{
+ const struct rule rules[] = {
+ {
+ .path = TMP_DIR,
+ .access = ACCESS_RW,
+ },
+ {},
+ };
+ static const char unrelated_dir[] = TMP_DIR "/s2d1/unrelated";
+ static const char unrelated_file[] = TMP_DIR "/s2d1/unrelated/f1";
+ int ruleset_fd;
+
+ ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
+ ASSERT_LE(0, ruleset_fd);
+
+ /* Adds a no-inherit rule on a leaf directory unrelated to s2. */
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d3,
+ LANDLOCK_ADD_RULE_NO_INHERIT);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ /* Ensure we can still create and delete files outside the sealed branch. */
+ ASSERT_EQ(0, mkdir(unrelated_dir, 0700));
+ ASSERT_EQ(0, mknod(unrelated_file, S_IFREG | 0600, 0));
+ ASSERT_EQ(0, unlink(unrelated_file));
+ ASSERT_EQ(0, rmdir(unrelated_dir));
+
+ /* Existing siblings in s2 remain modifiable. */
+ ASSERT_EQ(0, unlink(file1_s2d1));
+ ASSERT_EQ(0, mknod(file1_s2d1, S_IFREG | 0700, 0));
+}
+
+TEST_F_FORK(layout1, inherit_no_inherit_descendant_rw)
+{
+ const struct rule rules[] = {
+ {
+ .path = TMP_DIR,
+ .access = ACCESS_RO,
+ },
+ {},
+ };
+ const __u64 handled_access = ACCESS_RW | LANDLOCK_ACCESS_FS_MAKE_REG |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE;
+ static const char child_file[] =
+ TMP_DIR "/s1d1/s1d2/s1d3/rw_descendant";
+ int ruleset_fd;
+
+ ruleset_fd = create_ruleset(_metadata, handled_access, rules);
+ ASSERT_LE(0, ruleset_fd);
+
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d2,
+ LANDLOCK_ADD_RULE_NO_INHERIT);
+ add_path_beneath(_metadata, ruleset_fd,
+ ACCESS_RW | LANDLOCK_ACCESS_FS_MAKE_REG |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE,
+ dir_s1d3, 0);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ ASSERT_EQ(0, mknod(child_file, S_IFREG | 0600, 0));
+ ASSERT_EQ(0, unlink(child_file));
+}
+
+TEST_F_FORK(layout1, inherit_no_inherit_topology_file)
+{
+ const struct rule rules[] = {
+ {
+ .path = TMP_DIR,
+ .access = ACCESS_RW,
+ },
+ {},
+ };
+ int ruleset_fd;
+ struct landlock_path_beneath_attr path_beneath = {
+ .allowed_access = ACCESS_RO,
+ };
+
+ ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
+ ASSERT_LE(0, ruleset_fd);
+
+ path_beneath.parent_fd = open(file1_s1d2, O_PATH | O_CLOEXEC);
+ ASSERT_LE(0, path_beneath.parent_fd);
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath,
+ LANDLOCK_ADD_RULE_NO_INHERIT));
+ ASSERT_EQ(EINVAL, errno);
+ ASSERT_EQ(0, close(path_beneath.parent_fd));
+ ASSERT_EQ(0, close(ruleset_fd));
+}
+
+TEST_F_FORK(layout1, inherit_no_inherit_layered)
+{
+ const struct rule layer1[] = {
+ {
+ .path = TMP_DIR,
+ .access = ACCESS_RW | LANDLOCK_ACCESS_FS_REMOVE_FILE,
+ },
+ {},
+ };
+ int ruleset_fd;
+ static const char unrelated_dir[] = TMP_DIR "/s2d1/unrelated";
+ static const char unrelated_file[] = TMP_DIR "/s2d1/unrelated/f1";
+
+ /* Layer 1: RW on TMP_DIR */
+ ruleset_fd = create_ruleset(_metadata,
+ ACCESS_RW | LANDLOCK_ACCESS_FS_REMOVE_FILE,
+ layer1);
+ ASSERT_LE(0, ruleset_fd);
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ /* Layer 2: Add no-inherit RO rule on s1d2 */
+ ruleset_fd = create_ruleset(_metadata,
+ ACCESS_RW | LANDLOCK_ACCESS_FS_REMOVE_FILE,
+ layer1);
+ ASSERT_LE(0, ruleset_fd);
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d2,
+ LANDLOCK_ADD_RULE_NO_INHERIT);
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ /* Operations in unrelated areas should still work */
+ ASSERT_EQ(0, mkdir(unrelated_dir, 0700));
+ ASSERT_EQ(0, mknod(unrelated_file, S_IFREG | 0600, 0));
+ ASSERT_EQ(0, unlink(unrelated_file));
+ ASSERT_EQ(0, rmdir(unrelated_dir));
+
+ /* Creating in s1d1 should be allowed (parent still has RW) */
+ ASSERT_EQ(0, mknod(TMP_DIR "/s1d1/newfile", S_IFREG | 0600, 0));
+ ASSERT_EQ(0, unlink(TMP_DIR "/s1d1/newfile"));
+
+ /* Content of s1d2 should be read-only */
+ ASSERT_EQ(-1, unlink(file1_s1d2));
+ ASSERT_EQ(EACCES, errno);
+
+ /* Topology changes to s1d2 should be denied */
+ ASSERT_EQ(-1, rename(dir_s1d2, TMP_DIR "/s2d1/renamed"));
+ ASSERT_EQ(EACCES, errno);
+
+ /* Renaming s1d1 should also be denied (it's an ancestor) */
+ ASSERT_EQ(-1, rename(dir_s1d1, TMP_DIR "/s2d1/renamed"));
+ ASSERT_EQ(EACCES, errno);
+}
+
/* clang-format off */
FIXTURE(ioctl) {};
@@ -7088,6 +7429,100 @@ TEST_F(audit_layout1, write_file)
EXPECT_EQ(1, records.domain);
}
+TEST_F(audit_layout1, no_inherit_parent_is_logged)
+{
+ struct audit_records records;
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = ACCESS_RW,
+ };
+ int ruleset_fd;
+
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ /* Base read-only rule at s1d1. */
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d1, 0);
+ /* Descendant s1d1/s1d2/s1d3 forbids inheritance but should still log. */
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d3,
+ LANDLOCK_ADD_RULE_NO_INHERIT);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+
+ EXPECT_EQ(EACCES, test_open(file1_s1d2, O_WRONLY));
+ EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
+ "fs\\.write_file", file1_s1d2));
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(1, records.domain);
+
+ EXPECT_EQ(0, close(ruleset_fd));
+}
+
+TEST_F(audit_layout1, no_inherit_blocks_quiet_flag_inheritence)
+{
+ struct audit_records records;
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = ACCESS_RW,
+ .quiet_access_fs = ACCESS_RW,
+ };
+ int ruleset_fd;
+
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ /* Base read-only rule at tmp/s1d1 with quiet flag. */
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d1,
+ LANDLOCK_ADD_RULE_QUIET);
+ /* Descendant tmp/s1d1/s1d2/s1d3 forbids inheritance of quiet flag and should still log. */
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d3,
+ LANDLOCK_ADD_RULE_NO_INHERIT);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+
+ EXPECT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY));
+ EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
+ "fs\\.write_file", file1_s1d3));
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(1, records.domain);
+
+ EXPECT_EQ(0, close(ruleset_fd));
+}
+
+TEST_F(audit_layout1, no_inherit_quiet_parent)
+{
+ struct audit_records records;
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = ACCESS_RW,
+ .quiet_access_fs = ACCESS_RW,
+ };
+ int ruleset_fd;
+
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ /* Base read-only rule at tmp/s1d1 with quiet flag. */
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d1,
+ LANDLOCK_ADD_RULE_QUIET);
+ /* Access to dir_s1d1 shouldn't log */
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d3,
+ LANDLOCK_ADD_RULE_NO_INHERIT);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+
+ EXPECT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY));
+ EXPECT_NE(0, matches_log_fs(_metadata, self->audit_fd,
+ "fs\\.write_file", file1_s1d1));
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(0, records.domain);
+
+ EXPECT_EQ(0, close(ruleset_fd));
+}
+
TEST_F(audit_layout1, read_file)
{
struct audit_records records;
@@ -7647,7 +8082,7 @@ static int apply_a_layer(struct __test_metadata *const _metadata,
continue;
add_path_beneath(_metadata, rs_fd, r->access, r->path,
- r->quiet);
+ r->quiet ? LANDLOCK_ADD_RULE_QUIET : 0);
}
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
--
2.51.0
^ permalink raw reply related
* [PATCH v3 5/5] landlock: Implement KUnit test for LANDLOCK_ADD_RULE_NO_INHERIT
From: Justin Suess @ 2025-11-26 12:20 UTC (permalink / raw)
To: linux-security-module
Cc: Tingmao Wang, Günther Noack, Jan Kara, Abhinav Saxena,
Mickaël Salaün, Justin Suess
In-Reply-To: <20251126122039.3832162-1-utilityemal77@gmail.com>
Add a unit test for rule_flag collection, ensuring that access masks
are properly propagated with the flags.
changes v2..v3:
* Removing erroneously misplaced code and placed in the proper
patch.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
build.log
---
security/landlock/ruleset.c | 89 +++++++++++++++++++++++++++++++++++++
1 file changed, 89 insertions(+)
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index f7b6a48bbf39..0e0de8b20dc4 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -22,6 +22,7 @@
#include <linux/spinlock.h>
#include <linux/workqueue.h>
#include <uapi/linux/landlock.h>
+#include <kunit/test.h>
#include "access.h"
#include "audit.h"
@@ -774,3 +775,91 @@ landlock_init_layer_masks(const struct landlock_ruleset *const domain,
}
return handled_accesses;
}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+/**
+ * test_unmask_layers_no_inherit - Test landlock_unmask_layers() with no_inherit
+ * @rule_flags: Pointer to collected_rule_flags structure to track flags.
+ */
+static void test_unmask_layers_no_inherit(struct kunit *const test)
+{
+ struct landlock_rule *rule;
+ layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS];
+ struct collected_rule_flags rule_flags;
+ const access_mask_t access_request = BIT_ULL(0) | BIT_ULL(1);
+ const layer_mask_t layers_initialized = BIT_ULL(0) | BIT_ULL(1);
+ size_t i;
+
+ rule = kzalloc(struct_size(rule, layers, 2), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, rule);
+
+ rule->num_layers = 2;
+
+ /* Layer 1: allows access 0, no_inherit */
+ rule->layers[0].level = 1;
+ rule->layers[0].access = BIT_ULL(0);
+ rule->layers[0].flags.no_inherit = 1;
+
+ /* Layer 2: allows access 1 */
+ rule->layers[1].level = 2;
+ rule->layers[1].access = BIT_ULL(1);
+
+ /* Case 1: No rule_flags provided (should behave normally) */
+ for (i = 0; i < ARRAY_SIZE(layer_masks); i++)
+ layer_masks[i] = layers_initialized;
+
+ landlock_unmask_layers(rule, access_request, &layer_masks,
+ ARRAY_SIZE(layer_masks), NULL);
+
+ /* Access 0 should be unmasked by layer 1 */
+ KUNIT_EXPECT_EQ(test, layer_masks[0], layers_initialized & ~BIT_ULL(0));
+ /* Access 1 should be unmasked by layer 2 */
+ KUNIT_EXPECT_EQ(test, layer_masks[1], layers_initialized & ~BIT_ULL(1));
+
+ /* Case 2: rule_flags provided, no existing no_inherit_masks */
+ for (i = 0; i < ARRAY_SIZE(layer_masks); i++)
+ layer_masks[i] = layers_initialized;
+ memset(&rule_flags, 0, sizeof(rule_flags));
+
+ landlock_unmask_layers(rule, access_request, &layer_masks,
+ ARRAY_SIZE(layer_masks), &rule_flags);
+
+ /* Access 0 should be unmasked by layer 1 */
+ KUNIT_EXPECT_EQ(test, layer_masks[0], layers_initialized & ~BIT_ULL(0));
+ /* Access 1 should be unmasked by layer 2 */
+ KUNIT_EXPECT_EQ(test, layer_masks[1], layers_initialized & ~BIT_ULL(1));
+
+ /* rule_flags should collect no_inherit from layer 1 */
+ KUNIT_EXPECT_EQ(test, rule_flags.no_inherit_masks, (layer_mask_t)BIT_ULL(0));
+
+ /* Case 3: rule_flags provided, layer 1 is masked by no_inherit_masks */
+ for (i = 0; i < ARRAY_SIZE(layer_masks); i++)
+ layer_masks[i] = layers_initialized;
+ memset(&rule_flags, 0, sizeof(rule_flags));
+ rule_flags.no_inherit_masks = BIT_ULL(0); /* Mask layer 1 */
+
+ landlock_unmask_layers(rule, access_request, &layer_masks,
+ ARRAY_SIZE(layer_masks), &rule_flags);
+
+ /* Access 0 should NOT be unmasked by layer 1 because it is skipped */
+ KUNIT_EXPECT_EQ(test, layer_masks[0], layers_initialized);
+ /* Access 1 should be unmasked by layer 2 */
+ KUNIT_EXPECT_EQ(test, layer_masks[1], layers_initialized & ~BIT_ULL(1));
+
+ kfree(rule);
+}
+
+static struct kunit_case ruleset_test_cases[] = {
+ KUNIT_CASE(test_unmask_layers_no_inherit),
+ {}
+};
+
+static struct kunit_suite ruleset_test_suite = {
+ .name = "landlock_ruleset",
+ .test_cases = ruleset_test_cases,
+};
+
+kunit_test_suite(ruleset_test_suite);
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
--
2.51.0
^ permalink raw reply related
* [PATCH v3 5/5] landlock: landlock: Implement KUnit test for LANDLOCK_ADD_RULE_NO_INHERIT
From: Justin Suess @ 2025-11-26 12:20 UTC (permalink / raw)
To: linux-security-module
Cc: Tingmao Wang, Günther Noack, Jan Kara, Abhinav Saxena,
Mickaël Salaün, Justin Suess
In-Reply-To: <20251126122039.3832162-1-utilityemal77@gmail.com>
Add a unit test for rule_flag collection, ensuring that access masks
are properly propagated with the flags.
changes v2..v3:
* Removing erroneously misplaced code and placed in the proper
patch.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
security/landlock/ruleset.c | 89 +++++++++++++++++++++++++++++++++++++
1 file changed, 89 insertions(+)
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index f7b6a48bbf39..0e0de8b20dc4 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -22,6 +22,7 @@
#include <linux/spinlock.h>
#include <linux/workqueue.h>
#include <uapi/linux/landlock.h>
+#include <kunit/test.h>
#include "access.h"
#include "audit.h"
@@ -774,3 +775,91 @@ landlock_init_layer_masks(const struct landlock_ruleset *const domain,
}
return handled_accesses;
}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+/**
+ * test_unmask_layers_no_inherit - Test landlock_unmask_layers() with no_inherit
+ * @rule_flags: Pointer to collected_rule_flags structure to track flags.
+ */
+static void test_unmask_layers_no_inherit(struct kunit *const test)
+{
+ struct landlock_rule *rule;
+ layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS];
+ struct collected_rule_flags rule_flags;
+ const access_mask_t access_request = BIT_ULL(0) | BIT_ULL(1);
+ const layer_mask_t layers_initialized = BIT_ULL(0) | BIT_ULL(1);
+ size_t i;
+
+ rule = kzalloc(struct_size(rule, layers, 2), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, rule);
+
+ rule->num_layers = 2;
+
+ /* Layer 1: allows access 0, no_inherit */
+ rule->layers[0].level = 1;
+ rule->layers[0].access = BIT_ULL(0);
+ rule->layers[0].flags.no_inherit = 1;
+
+ /* Layer 2: allows access 1 */
+ rule->layers[1].level = 2;
+ rule->layers[1].access = BIT_ULL(1);
+
+ /* Case 1: No rule_flags provided (should behave normally) */
+ for (i = 0; i < ARRAY_SIZE(layer_masks); i++)
+ layer_masks[i] = layers_initialized;
+
+ landlock_unmask_layers(rule, access_request, &layer_masks,
+ ARRAY_SIZE(layer_masks), NULL);
+
+ /* Access 0 should be unmasked by layer 1 */
+ KUNIT_EXPECT_EQ(test, layer_masks[0], layers_initialized & ~BIT_ULL(0));
+ /* Access 1 should be unmasked by layer 2 */
+ KUNIT_EXPECT_EQ(test, layer_masks[1], layers_initialized & ~BIT_ULL(1));
+
+ /* Case 2: rule_flags provided, no existing no_inherit_masks */
+ for (i = 0; i < ARRAY_SIZE(layer_masks); i++)
+ layer_masks[i] = layers_initialized;
+ memset(&rule_flags, 0, sizeof(rule_flags));
+
+ landlock_unmask_layers(rule, access_request, &layer_masks,
+ ARRAY_SIZE(layer_masks), &rule_flags);
+
+ /* Access 0 should be unmasked by layer 1 */
+ KUNIT_EXPECT_EQ(test, layer_masks[0], layers_initialized & ~BIT_ULL(0));
+ /* Access 1 should be unmasked by layer 2 */
+ KUNIT_EXPECT_EQ(test, layer_masks[1], layers_initialized & ~BIT_ULL(1));
+
+ /* rule_flags should collect no_inherit from layer 1 */
+ KUNIT_EXPECT_EQ(test, rule_flags.no_inherit_masks, (layer_mask_t)BIT_ULL(0));
+
+ /* Case 3: rule_flags provided, layer 1 is masked by no_inherit_masks */
+ for (i = 0; i < ARRAY_SIZE(layer_masks); i++)
+ layer_masks[i] = layers_initialized;
+ memset(&rule_flags, 0, sizeof(rule_flags));
+ rule_flags.no_inherit_masks = BIT_ULL(0); /* Mask layer 1 */
+
+ landlock_unmask_layers(rule, access_request, &layer_masks,
+ ARRAY_SIZE(layer_masks), &rule_flags);
+
+ /* Access 0 should NOT be unmasked by layer 1 because it is skipped */
+ KUNIT_EXPECT_EQ(test, layer_masks[0], layers_initialized);
+ /* Access 1 should be unmasked by layer 2 */
+ KUNIT_EXPECT_EQ(test, layer_masks[1], layers_initialized & ~BIT_ULL(1));
+
+ kfree(rule);
+}
+
+static struct kunit_case ruleset_test_cases[] = {
+ KUNIT_CASE(test_unmask_layers_no_inherit),
+ {}
+};
+
+static struct kunit_suite ruleset_test_suite = {
+ .name = "landlock_ruleset",
+ .test_cases = ruleset_test_cases,
+};
+
+kunit_test_suite(ruleset_test_suite);
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
--
2.51.0
^ permalink raw reply related
* Re: [PATCH 0/2] apparmor unaligned memory fixes
From: david laight @ 2025-11-26 14:22 UTC (permalink / raw)
To: Helge Deller
Cc: John Johansen, Helge Deller, John Paul Adrian Glaubitz,
linux-kernel, apparmor, linux-security-module, linux-parisc
In-Reply-To: <4034ad19-8e09-440c-a042-a66a488c048b@gmx.de>
On Wed, 26 Nov 2025 12:03:03 +0100
Helge Deller <deller@gmx.de> wrote:
> On 11/26/25 11:44, david laight wrote:
...
> >> diff --git a/security/apparmor/match.c b/security/apparmor/match.c
> >> index 26e82ba879d44..3dcc342337aca 100644
> >> --- a/security/apparmor/match.c
> >> +++ b/security/apparmor/match.c
> >> @@ -71,10 +71,10 @@ static struct table_header *unpack_table(char *blob, size_t bsize)
> >> u8, u8, byte_to_byte);
> >
> > Is that that just memcpy() ?
>
> No, it's memcpy() only on big-endian machines.
You've misread the quoting...
The 'data8' case that was only half there is a memcpy().
> On little-endian machines it converts from big-endian
> 16/32-bit ints to little-endian 16/32-bit ints.
>
> But I see some potential for optimization here:
> a) on big-endian machines just use memcpy()
true
> b) on little-endian machines use memcpy() to copy from possibly-unaligned
> memory to then known-to-be-aligned destination. Then use a loop with
> be32_to_cpu() instead of get_unaligned_xx() as it's faster.
There is a function that does a loop byteswap of a buffer - no reason
to re-invent it.
But I doubt it is always (if ever) faster to do a copy and then byteswap.
The loop control and extra memory accesses kill performance.
Not that I've seen a fast get_unaligned() - I don't think gcc or clang
generate optimal code - For LE I think it is something like:
low = *(addr & ~3);
high = *((addr + 3) & ~3);
shift = (addr & 3) * 8;
value = low << shift | high >> (32 - shift);
Note that it is only 2 aligned memory reads - even for 64bit.
David
^ permalink raw reply
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