* Re: [PATCH v5 10/11] lsm: consolidate all of the LSM framework initcalls
From: Lorenzo Stoakes @ 2026-01-29 17:09 UTC (permalink / raw)
To: Vlastimil Babka
Cc: Paul Moore, linux-security-module, linux-integrity, selinux,
john.johansen, zohar, roberto.sassu, wufan, mic, gnoack, kees,
mortonm, casey, penguin-kernel, nicolas.bouchinet, xiujianfeng,
linux-mm, David Hildenbrand, Liam R. Howlett, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel
In-Reply-To: <74286aca-a565-489f-ad2c-886c650ea2bc@suse.cz>
On Thu, Jan 29, 2026 at 06:02:00PM +0100, Vlastimil Babka wrote:
> Agreed, the mmap_min_addr should stay visible and applied unconditionally.
> AFAICS the only relation to SECURITY/LSM is whether CONFIG_LSM_MMAP_MIN_ADDR
> is used as an additional lower limit to both CONFIG_DEFAULT_MMAP_MIN_ADDR
> and the sysctl-written value?
Thanks, yeah we should probably actually move the non-LSM-relevant stuff
out to mm to be honest.
But that's future work, for an -rc8 hotfix we need to make the init of this
particular module not dependent on normal LSM initialisation, as horrid as
that is...
Cheers, Lorenzo
^ permalink raw reply
* [PATCH v4] ima: Fallback to ctime check for FS without kstat.change_cookie
From: Frederick Lawler @ 2026-01-29 18:07 UTC (permalink / raw)
To: Mimi Zohar, Roberto Sassu, Dmitry Kasatkin, Eric Snowberg,
Paul Moore, James Morris, Serge E. Hallyn, Darrick J. Wong,
Christian Brauner, Josef Bacik, Jeff Layton
Cc: linux-kernel, linux-integrity, linux-security-module, kernel-team,
Frederick Lawler
Commit 1cf7e834a6fb ("xfs: switch to multigrain timestamps")
introduced a means to track change detection for an inode
via ctime updates, opposed to setting kstat.change_cookie to
an i_version when calling into xfs_vn_getattr().
This introduced a regression for IMA such that an action
performed on a LOWER inode on a stacked file systems always
requires a re-evaluation if the LOWER file system does not
leverage kstat.change_cookie to track inode i_version or lacks
i_version support all together.
In the case of stacking XFS on XFS, an action on either the LOWER or UPPER
will require re-evaluation. Stacking TMPFS on XFS for instance, once the
inode is UPPER is mutated, IMA resumes normal behavior because TMPFS
leverages generic_fillattr() to update the change cookie.
This is because IMA caches kstat.change_cookie to compare against an
inode's i_version directly in integrity_inode_attrs_changed(), and thus
could be out of date depending on how file systems set
kstat.change_cookie.
To address this, require integrity_inode_attrs_changed() to query
vfs_getattr_nosec() to compare the cached version against
kstat.change_cookie directly. This ensures that when updates occur,
we're accessing the same changed inode version on changes, and fallback
to compare against kstat.ctime when STATX_CHANGE_COOKIE is missing from
result mask.
Lastly, because EVM still relies on querying and caching a inode's
i_version directly, the integrity_inode_attrs_changed() falls back to the
original inode.i_version != cached comparison.
Link: https://lore.kernel.org/all/aTspr4_h9IU4EyrR@CMGLRV3
Fixes: 1cf7e834a6fb ("xfs: switch to multigrain timestamps")
Suggested-by: Jeff Layton <jlayton@kernel.org>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Signed-off-by: Frederick Lawler <fred@cloudflare.com>
---
We uncovered a case in kernels >= 6.13 where XFS is no longer updating
struct kstat.change_cookie on i_op getattr() access calls. Instead, XFS is
using multigrain ctime (as well as other file systems) for
change detection in commit 1cf7e834a6fb ("xfs: switch to
multigrain timestamps").
Because file systems may implement i_version as they see fit, IMA
caching may be behind as well as file systems that don't support/export
i_version. Thus we're proposing to compare against the kstat.change_cookie
directly to the cached version, and fall back to a ctime guard when
that's not updated.
EVM is largely left alone since there's no trivial way to query a file
directly in the LSM call paths to obtain kstat.change_cookie &
kstat.ctime to cache. Thus retains accessing i_version directly.
Regression tests will be added to the Linux Test Project instead of
selftest to help catch future file system changes that may impact
future evaluation of IMA.
I'd like this to be backported to at least 6.18 if possible.
Below is a simplified test that demonstrates the issue:
_fragment.config_
CONFIG_XFS_FS=y
CONFIG_OVERLAY_FS=y
CONFIG_IMA=y
CONFIG_IMA_WRITE_POLICY=y
CONFIG_IMA_READ_POLICY=y
_./test.sh_
IMA_POLICY="/sys/kernel/security/ima/policy"
TEST_BIN="/bin/date"
MNT_BASE="/tmp/ima_test_root"
mkdir -p "$MNT_BASE"
mount -t tmpfs tmpfs "$MNT_BASE"
mkdir -p "$MNT_BASE"/{xfs_disk,upper,work,ovl}
dd if=/dev/zero of="$MNT_BASE/xfs.img" bs=1M count=300
mkfs.xfs -q "$MNT_BASE/xfs.img"
mount "$MNT_BASE/xfs.img" "$MNT_BASE/xfs_disk"
cp "$TEST_BIN" "$MNT_BASE/xfs_disk/test_prog"
mount -t overlay overlay -o \
"lowerdir=$MNT_BASE/xfs_disk,upperdir=$MNT_BASE/upper,workdir=$MNT_BASE/work" \
"$MNT_BASE/ovl"
echo "audit func=BPRM_CHECK uid=$(id -u nobody)" > "$IMA_POLICY"
target_prog="$MNT_BASE/ovl/test_prog"
setpriv --reuid nobody "$target_prog"
setpriv --reuid nobody "$target_prog"
setpriv --reuid nobody "$target_prog"
audit_count=$(dmesg | grep -c "file=\"$target_prog\"")
if [[ "$audit_count" -eq 1 ]]; then
echo "PASS: Found exactly 1 audit event."
else
echo "FAIL: Expected 1 audit event, but found $audit_count."
exit 1
fi
---
Changes in v4:
- No functional changes.
- Add Reviewed-by & Fixes tags.
- Link to v3: https://lore.kernel.org/r/20260122-xfs-ima-fixup-v3-1-20335a8aa836@cloudflare.com
Changes in v3:
- Prefer timespec64_to_ns() to leverage attr.version. [Roberto]
- s/TPMFS/TMPFS/ in description.
- Link to v2: https://lore.kernel.org/r/20260120-xfs-ima-fixup-v2-1-f332ead8b043@cloudflare.com
Changes in v2:
- Updated commit description + message to clarify the problem.
- compare struct timespec64 to avoid collision possibility [Roberto].
- Don't check inode_attr_changed() in ima_check_last_writer()
- Link to v1: https://lore.kernel.org/r/20260112-xfs-ima-fixup-v1-1-8d13b6001312@cloudflare.com
Changes since RFC:
- Remove calls to I_IS_VERSION()
- Function documentation/comments
- Abide IMA/EVM change detection fallback invariants
- Combined ctime guard into version for attributes struct
- Link to RFC: https://lore.kernel.org/r/20251229-xfs-ima-fixup-v1-1-6a717c939f7c@cloudflare.com
---
include/linux/integrity.h | 35 +++++++++++++++++++++++++++++++----
security/integrity/evm/evm_main.c | 5 ++---
security/integrity/ima/ima_api.c | 11 ++++++++---
security/integrity/ima/ima_main.c | 17 ++++++++++-------
4 files changed, 51 insertions(+), 17 deletions(-)
diff --git a/include/linux/integrity.h b/include/linux/integrity.h
index f5842372359be5341b6870a43b92e695e8fc78af..034f0a1ed48ca8c19c764e302bbfc555dad92cde 100644
--- a/include/linux/integrity.h
+++ b/include/linux/integrity.h
@@ -9,6 +9,8 @@
#include <linux/fs.h>
#include <linux/iversion.h>
+#include <linux/kernel.h>
+#include <linux/time64.h>
enum integrity_status {
INTEGRITY_PASS = 0,
@@ -51,14 +53,39 @@ integrity_inode_attrs_store(struct integrity_inode_attributes *attrs,
/*
* On stacked filesystems detect whether the inode or its content has changed.
+ *
+ * Must be called in process context.
*/
static inline bool
integrity_inode_attrs_changed(const struct integrity_inode_attributes *attrs,
- const struct inode *inode)
+ struct file *file, struct inode *inode)
{
- return (inode->i_sb->s_dev != attrs->dev ||
- inode->i_ino != attrs->ino ||
- !inode_eq_iversion(inode, attrs->version));
+ struct kstat stat;
+
+ might_sleep();
+
+ if (inode->i_sb->s_dev != attrs->dev || inode->i_ino != attrs->ino)
+ return true;
+
+ /*
+ * EVM currently relies on backing inode i_version. While IS_I_VERSION
+ * is not a good indicator of i_version support, this still retains
+ * the logic such that a re-evaluation should still occur for EVM, and
+ * only for IMA if vfs_getattr_nosec() fails.
+ */
+ if (!file || vfs_getattr_nosec(&file->f_path, &stat,
+ STATX_CHANGE_COOKIE | STATX_CTIME,
+ AT_STATX_SYNC_AS_STAT))
+ return !IS_I_VERSION(inode) ||
+ !inode_eq_iversion(inode, attrs->version);
+
+ if (stat.result_mask & STATX_CHANGE_COOKIE)
+ return stat.change_cookie != attrs->version;
+
+ if (stat.result_mask & STATX_CTIME)
+ return timespec64_to_ns(&stat.ctime) != (s64)attrs->version;
+
+ return true;
}
diff --git a/security/integrity/evm/evm_main.c b/security/integrity/evm/evm_main.c
index 73d500a375cb37a54f295b0e1e93fd6e5d9ecddc..6a4e0e246005246d5700b1db590c1759242b9cb6 100644
--- a/security/integrity/evm/evm_main.c
+++ b/security/integrity/evm/evm_main.c
@@ -752,9 +752,8 @@ bool evm_metadata_changed(struct inode *inode, struct inode *metadata_inode)
bool ret = false;
if (iint) {
- ret = (!IS_I_VERSION(metadata_inode) ||
- integrity_inode_attrs_changed(&iint->metadata_inode,
- metadata_inode));
+ ret = integrity_inode_attrs_changed(&iint->metadata_inode,
+ NULL, metadata_inode);
if (ret)
iint->evm_status = INTEGRITY_UNKNOWN;
}
diff --git a/security/integrity/ima/ima_api.c b/security/integrity/ima/ima_api.c
index c35ea613c9f8d404ba4886e3b736c3bab29d1668..e47d6281febc15a0ac1bd2ea1d28fea4d0cd5c58 100644
--- a/security/integrity/ima/ima_api.c
+++ b/security/integrity/ima/ima_api.c
@@ -272,10 +272,15 @@ int ima_collect_measurement(struct ima_iint_cache *iint, struct file *file,
* to an initial measurement/appraisal/audit, but was modified to
* assume the file changed.
*/
- result = vfs_getattr_nosec(&file->f_path, &stat, STATX_CHANGE_COOKIE,
+ result = vfs_getattr_nosec(&file->f_path, &stat,
+ STATX_CHANGE_COOKIE | STATX_CTIME,
AT_STATX_SYNC_AS_STAT);
- if (!result && (stat.result_mask & STATX_CHANGE_COOKIE))
- i_version = stat.change_cookie;
+ if (!result) {
+ if (stat.result_mask & STATX_CHANGE_COOKIE)
+ i_version = stat.change_cookie;
+ else if (stat.result_mask & STATX_CTIME)
+ i_version = timespec64_to_ns(&stat.ctime);
+ }
hash.hdr.algo = algo;
hash.hdr.length = hash_digest_size[algo];
diff --git a/security/integrity/ima/ima_main.c b/security/integrity/ima/ima_main.c
index 5770cf691912aa912fc65280c59f5baac35dd725..8ac42b03740eb93bf23b15cb9039af6cd32aa999 100644
--- a/security/integrity/ima/ima_main.c
+++ b/security/integrity/ima/ima_main.c
@@ -28,6 +28,7 @@
#include <linux/iversion.h>
#include <linux/evm.h>
#include <linux/crash_dump.h>
+#include <linux/time64.h>
#include "ima.h"
@@ -199,10 +200,13 @@ static void ima_check_last_writer(struct ima_iint_cache *iint,
&iint->atomic_flags);
if ((iint->flags & IMA_NEW_FILE) ||
vfs_getattr_nosec(&file->f_path, &stat,
- STATX_CHANGE_COOKIE,
- AT_STATX_SYNC_AS_STAT) ||
- !(stat.result_mask & STATX_CHANGE_COOKIE) ||
- stat.change_cookie != iint->real_inode.version) {
+ STATX_CHANGE_COOKIE | STATX_CTIME,
+ AT_STATX_SYNC_AS_STAT) ||
+ ((stat.result_mask & STATX_CHANGE_COOKIE) ?
+ stat.change_cookie != iint->real_inode.version :
+ (!(stat.result_mask & STATX_CTIME) ||
+ timespec64_to_ns(&stat.ctime) !=
+ (s64)iint->real_inode.version))) {
iint->flags &= ~(IMA_DONE_MASK | IMA_NEW_FILE);
iint->measured_pcrs = 0;
if (update)
@@ -328,9 +332,8 @@ static int process_measurement(struct file *file, const struct cred *cred,
real_inode = d_real_inode(file_dentry(file));
if (real_inode != inode &&
(action & IMA_DO_MASK) && (iint->flags & IMA_DONE_MASK)) {
- if (!IS_I_VERSION(real_inode) ||
- integrity_inode_attrs_changed(&iint->real_inode,
- real_inode)) {
+ if (integrity_inode_attrs_changed(&iint->real_inode,
+ file, real_inode)) {
iint->flags &= ~IMA_DONE_MASK;
iint->measured_pcrs = 0;
}
---
base-commit: 8f0b4cce4481fb22653697cced8d0d04027cb1e8
change-id: 20251212-xfs-ima-fixup-931780a62c2c
Best regards,
--
Frederick Lawler <fred@cloudflare.com>
^ permalink raw reply related
* Re: [PATCH v5 10/11] lsm: consolidate all of the LSM framework initcalls
From: Paul Moore @ 2026-01-29 18:31 UTC (permalink / raw)
To: Lorenzo Stoakes
Cc: Vlastimil Babka, linux-security-module, linux-integrity, selinux,
john.johansen, zohar, roberto.sassu, wufan, mic, gnoack, kees,
mortonm, casey, penguin-kernel, nicolas.bouchinet, xiujianfeng,
linux-mm, David Hildenbrand, Liam R. Howlett, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel
In-Reply-To: <01cb28cb-56b7-4862-bf27-07e4bf17115e@lucifer.local>
On Thu, Jan 29, 2026 at 12:11 PM Lorenzo Stoakes
<lorenzo.stoakes@oracle.com> wrote:
>
> On Thu, Jan 29, 2026 at 06:02:00PM +0100, Vlastimil Babka wrote:
> > Agreed, the mmap_min_addr should stay visible and applied unconditionally.
> > AFAICS the only relation to SECURITY/LSM is whether CONFIG_LSM_MMAP_MIN_ADDR
> > is used as an additional lower limit to both CONFIG_DEFAULT_MMAP_MIN_ADDR
> > and the sysctl-written value?
>
> Thanks, yeah we should probably actually move the non-LSM-relevant stuff
> out to mm to be honest.
Yes, definitely. Send the LSM and VM lists some patches after the
upcoming merge window closes and I'll make sure they are merged once
fully ACK'd.
> But that's future work, for an -rc8 hotfix ...
Expect a patch later today.
--
paul-moore.com
^ permalink raw reply
* Re: [PATCH v5 10/11] lsm: consolidate all of the LSM framework initcalls
From: Lorenzo Stoakes @ 2026-01-29 18:58 UTC (permalink / raw)
To: Paul Moore
Cc: Vlastimil Babka, linux-security-module, linux-integrity, selinux,
john.johansen, zohar, roberto.sassu, wufan, mic, gnoack, kees,
mortonm, casey, penguin-kernel, nicolas.bouchinet, xiujianfeng,
linux-mm, David Hildenbrand, Liam R. Howlett, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel
In-Reply-To: <CAHC9VhS_y2gy2nAK7C-OpchME0pzS34-QN2bY9t5SRBXArz8Xw@mail.gmail.com>
On Thu, Jan 29, 2026 at 01:31:05PM -0500, Paul Moore wrote:
> On Thu, Jan 29, 2026 at 12:11 PM Lorenzo Stoakes
> <lorenzo.stoakes@oracle.com> wrote:
> >
> > On Thu, Jan 29, 2026 at 06:02:00PM +0100, Vlastimil Babka wrote:
> > > Agreed, the mmap_min_addr should stay visible and applied unconditionally.
> > > AFAICS the only relation to SECURITY/LSM is whether CONFIG_LSM_MMAP_MIN_ADDR
> > > is used as an additional lower limit to both CONFIG_DEFAULT_MMAP_MIN_ADDR
> > > and the sysctl-written value?
> >
> > Thanks, yeah we should probably actually move the non-LSM-relevant stuff
> > out to mm to be honest.
>
> Yes, definitely. Send the LSM and VM lists some patches after the
> upcoming merge window closes and I'll make sure they are merged once
> fully ACK'd.
Great thank you! Will add to todo. I think that's a sensible thing we can
do to help you keep this code sane.
>
> > But that's future work, for an -rc8 hotfix ...
>
> Expect a patch later today.
Perfect thank you very much!
>
> --
> paul-moore.com
Cheers, Lorenzo
^ permalink raw reply
* Re: [PATCH v2 3/3] landlock: transpose the layer masks data structure
From: Mickaël Salaün @ 2026-01-29 20:28 UTC (permalink / raw)
To: Günther Noack
Cc: linux-security-module, Tingmao Wang, Justin Suess,
Samasth Norway Ananda, Matthieu Buffet, Mikhail Ivanov,
konstantin.meskhidze, Randy Dunlap
In-Reply-To: <20260125195853.109967-4-gnoack3000@gmail.com>
On Sun, Jan 25, 2026 at 08:58:53PM +0100, Günther Noack wrote:
> The layer masks data structure tracks the requested but unfulfilled
> access rights during an operation's security check. It stores one bit
> for each combination of access right and layer index. If the bit is
> set, that access right is not granted (yet) in the given layer and we
> have to traverse the path further upwards to grant it.
>
> Previously, the layer masks were stored as arrays mapping from access
> right indices to layer_mask_t. The layer_mask_t value then indicates
> all layers in which the given access right is still (tentatively)
> denied.
>
> This patch introduces struct layer_access_masks instead: This struct
> contains an array with the access_mask_t of each (tentatively) denied
> access right in that layer.
>
> The hypothesis of this patch is that this simplifies the code enough
> so that the resulting code will run faster:
>
> * We can use bitwise operations in multiple places where we previously
> looped over bits individually with macros. (Should require less
> branch speculation and lends itself to better loop unrolling.)
>
> * Code is ~75 lines smaller.
>
> Other noteworthy changes:
>
> * In no_more_access(), call a new helper function may_refer(), which
> only solves the asymmetric case. Previously, the code interleaved
> the checks for the two symmetric cases in RENAME_EXCHANGE. It feels
> that the code is clearer when renames without RENAME_EXCHANGE are
> more obviously the normal case.
>
> Tradeoffs:
>
> This change improves performance, at a slight size increase to the
> layer masks data structure.
>
> At the moment, for the filesystem access rights, the data structure
> has the same size as before, but once we introduce the 17th filesystem
> access right, it will double in size (from 32 to 64 bytes), as
> access_mask_t grows from 16 to 32 bit. See the link below for
> measurements.
>
> Link: https://lore.kernel.org/all/20260120.haeCh4li9Vae@digikod.net/
When adding extra links, please add a [1] reference at the end and use
this reference in the commit message.
> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> ---
> security/landlock/access.h | 10 +-
> security/landlock/audit.c | 84 +++------
> security/landlock/audit.h | 3 +-
> security/landlock/domain.c | 45 +++--
> security/landlock/domain.h | 4 +-
> security/landlock/fs.c | 352 ++++++++++++++++--------------------
> security/landlock/net.c | 11 +-
> security/landlock/ruleset.c | 88 ++++-----
> security/landlock/ruleset.h | 21 ++-
> 9 files changed, 274 insertions(+), 344 deletions(-)
^ permalink raw reply
* Re: [PATCH v2 1/6] landlock: Add LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET scope bit to uAPI
From: Mickaël Salaün @ 2026-01-29 21:27 UTC (permalink / raw)
To: Tingmao Wang
Cc: Günther Noack, Demi Marie Obenour, Alyssa Ross, Jann Horn,
Tahera Fahimi, Justin Suess, linux-security-module
In-Reply-To: <88f914eb2024fa1fffe219e34c33a835b31730b8.1767115163.git.m@maowtm.org>
On Tue, Dec 30, 2025 at 05:20:19PM +0000, Tingmao Wang wrote:
> Add the new scope bit to the uAPI header, add documentation, and bump ABI
> version to 8.
This patch and the next one should be fold together. If a new UAPI is
added, it should come with the kernel implementation.
>
> This documentation edit specifically calls out the security implications of
> not restricting sockets.
>
> Fix some minor cosmetic issue in landlock.h around the changed lines as
> well.
>
> Signed-off-by: Tingmao Wang <m@maowtm.org>
> ---
>
> Changes in v2:
> - Fix grammar
>
> Note that in the code block in "Defining and enforcing a security policy"
> the switch case currently jumps from 5 to 7. This should be fixed by
> https://lore.kernel.org/all/20251216210248.4150777-1-samasth.norway.ananda@oracle.com/
>
> Documentation/userspace-api/landlock.rst | 37 ++++++++++++++++---
> include/uapi/linux/landlock.h | 8 +++-
> security/landlock/limits.h | 2 +-
> security/landlock/syscalls.c | 2 +-
> tools/testing/selftests/landlock/base_test.c | 2 +-
> .../testing/selftests/landlock/scoped_test.c | 2 +-
> 6 files changed, 42 insertions(+), 11 deletions(-)
>
> diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
> index 1d0c2c15c22e..5620a2be1091 100644
> --- a/Documentation/userspace-api/landlock.rst
> +++ b/Documentation/userspace-api/landlock.rst
> @@ -83,7 +83,8 @@ to be explicit about the denied-by-default access rights.
> LANDLOCK_ACCESS_NET_CONNECT_TCP,
> .scoped =
> LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
> - LANDLOCK_SCOPE_SIGNAL,
> + LANDLOCK_SCOPE_SIGNAL |
> + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET,
> };
>
> Because we may not know which kernel version an application will be executed
> @@ -127,6 +128,10 @@ version, and only use the available subset of access rights:
> /* Removes LANDLOCK_SCOPE_* for ABI < 6 */
> ruleset_attr.scoped &= ~(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
> LANDLOCK_SCOPE_SIGNAL);
> + __attribute__((fallthrough));
> + case 7:
> + /* Removes LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET for ABI < 8 */
> + ruleset_attr.scoped &= ~LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
> }
>
> This enables the creation of an inclusive ruleset that will contain our rules.
> @@ -328,10 +333,15 @@ The operations which can be scoped are:
> This limits the sending of signals to target processes which run within the
> same or a nested Landlock domain.
>
> -``LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET``
> - This limits the set of abstract :manpage:`unix(7)` sockets to which we can
> - :manpage:`connect(2)` to socket addresses which were created by a process in
> - the same or a nested Landlock domain.
> +``LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET`` and ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET``
> + This limits the set of :manpage:`unix(7)` sockets to which we can
> + :manpage:`connect(2)` to socket addresses which were created by a
> + process in the same or a nested Landlock domain.
> + ``LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET`` applies to abstract sockets,
> + and ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` applies to pathname
> + sockets.
The following part is not needed:
> Even though pathname sockets are represented in the
> + filesystem, Landlock filesystem rules do not currently control access
> + to them.
>
> A :manpage:`sendto(2)` on a non-connected datagram socket is treated as if
> it were doing an implicit :manpage:`connect(2)` and will be blocked if the
> @@ -604,6 +614,23 @@ Landlock audit events with the ``LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF``,
> sys_landlock_restrict_self(). See Documentation/admin-guide/LSM/landlock.rst
> for more details on audit.
>
> +Pathname UNIX socket (ABI < 8)
> +------------------------------
> +
> +Starting with the Landlock ABI version 8, it is possible to restrict
> +connections to a pathname (non-abstract) :manpage:`unix(7)` socket by
> +setting ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` to the ``scoped`` ruleset
> +attribute. This works the same way as the abstract socket scoping.
> +
> +This allows sandboxing applications using only Landlock to protect against
> +bypasses relying on connecting to Unix sockets of other services running
> +under the same user. These services typically assume that any process
> +capable of connecting to a local Unix socket, or connecting with the
> +expected user credentials, is trusted. Without this protection, sandbox
> +escapes may be possible, especially when running in a standard desktop
> +environment, such as by using systemd-run, or sockets exposed by other
> +common applications.
> +
> .. _kernel_support:
>
> Kernel support
> diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
> index f030adc462ee..590c6d4171a0 100644
> --- a/include/uapi/linux/landlock.h
> +++ b/include/uapi/linux/landlock.h
> @@ -364,10 +364,14 @@ struct landlock_net_port_attr {
> * related Landlock domain (e.g., a parent domain or a non-sandboxed process).
> * - %LANDLOCK_SCOPE_SIGNAL: Restrict a sandboxed process from sending a signal
> * to another process outside the domain.
> + * - %LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET: Restrict a sandboxed process from
> + * connecting to a pathname UNIX socket created by a process outside the
> + * related Landlock domain.
> */
> /* clang-format off */
> #define LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET (1ULL << 0)
> -#define LANDLOCK_SCOPE_SIGNAL (1ULL << 1)
> -/* clang-format on*/
> +#define LANDLOCK_SCOPE_SIGNAL (1ULL << 1)
> +#define LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET (1ULL << 2)
> +/* clang-format on */
>
> #endif /* _UAPI_LINUX_LANDLOCK_H */
> diff --git a/security/landlock/limits.h b/security/landlock/limits.h
> index 65b5ff051674..d653e14dba10 100644
> --- a/security/landlock/limits.h
> +++ b/security/landlock/limits.h
> @@ -27,7 +27,7 @@
> #define LANDLOCK_MASK_ACCESS_NET ((LANDLOCK_LAST_ACCESS_NET << 1) - 1)
> #define LANDLOCK_NUM_ACCESS_NET __const_hweight64(LANDLOCK_MASK_ACCESS_NET)
>
> -#define LANDLOCK_LAST_SCOPE LANDLOCK_SCOPE_SIGNAL
> +#define LANDLOCK_LAST_SCOPE LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
> #define LANDLOCK_MASK_SCOPE ((LANDLOCK_LAST_SCOPE << 1) - 1)
> #define LANDLOCK_NUM_SCOPE __const_hweight64(LANDLOCK_MASK_SCOPE)
>
> diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
> index 0116e9f93ffe..66fd196be85a 100644
> --- a/security/landlock/syscalls.c
> +++ b/security/landlock/syscalls.c
> @@ -161,7 +161,7 @@ static const struct file_operations ruleset_fops = {
> * Documentation/userspace-api/landlock.rst should be updated to reflect the
> * UAPI change.
> */
> -const int landlock_abi_version = 7;
> +const int landlock_abi_version = 8;
>
> /**
> * sys_landlock_create_ruleset - Create a new ruleset
> diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
> index 7b69002239d7..f4b1a275d8d9 100644
> --- a/tools/testing/selftests/landlock/base_test.c
> +++ b/tools/testing/selftests/landlock/base_test.c
> @@ -76,7 +76,7 @@ TEST(abi_version)
> const struct landlock_ruleset_attr ruleset_attr = {
> .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
> };
> - ASSERT_EQ(7, landlock_create_ruleset(NULL, 0,
> + ASSERT_EQ(8, landlock_create_ruleset(NULL, 0,
> LANDLOCK_CREATE_RULESET_VERSION));
>
> ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
> diff --git a/tools/testing/selftests/landlock/scoped_test.c b/tools/testing/selftests/landlock/scoped_test.c
> index b90f76ed0d9c..7f83512a328d 100644
> --- a/tools/testing/selftests/landlock/scoped_test.c
> +++ b/tools/testing/selftests/landlock/scoped_test.c
> @@ -12,7 +12,7 @@
>
> #include "common.h"
>
> -#define ACCESS_LAST LANDLOCK_SCOPE_SIGNAL
> +#define ACCESS_LAST LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
>
> TEST(ruleset_with_unknown_scope)
> {
> --
> 2.52.0
>
^ permalink raw reply
* Re: [PATCH v2 3/6] samples/landlock: Support LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
From: Mickaël Salaün @ 2026-01-29 21:27 UTC (permalink / raw)
To: Tingmao Wang
Cc: Günther Noack, Demi Marie Obenour, Alyssa Ross, Jann Horn,
Tahera Fahimi, Justin Suess, linux-security-module
In-Reply-To: <5e9ac4104e5f70cdf250a3dea9a553a65a36fff9.1767115163.git.m@maowtm.org>
We should have a (potentially small) description of what this patch
does, even if it's a bit redundant with the subject.
On Tue, Dec 30, 2025 at 05:20:21PM +0000, Tingmao Wang wrote:
> Signed-off-by: Tingmao Wang <m@maowtm.org>
> ---
>
> I've decided to use "u" as the character to control this scope bit since
> it stands for (normal) Unix sockets. Imo using "p" or "n" would make it less
> clear / memorable. Open to suggestions.
Looks good to me.
>
> Also, open to suggestion whether socket scoping (pathname and abstract)
> should be enabled by default, if LL_SCOPED is not set. This would break
> backward compatibility, but maybe we shouldn't guarentee backward
> compatibility of this sandboxer in the first place, and almost all cases
> of Landlock usage would want socket scoping.
I agree that this example could have better defaults, but this should be
done with a standalone patch series. An important point to keep in mind
is that this example is used by developers (e.g. potential copy/paste),
so we need to be careful to not encourage them to create code which is
backward incompatible. I think the best way to do it is to request a
default behavior for a specific Landlock ABI version (e.g. with a new
parameter).
I'd also like this example to still be simple to understand, update, and
maintain.
>
> samples/landlock/sandboxer.c | 23 ++++++++++++++++++-----
> 1 file changed, 18 insertions(+), 5 deletions(-)
>
> diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
> index e7af02f98208..2de14e1c787d 100644
> --- a/samples/landlock/sandboxer.c
> +++ b/samples/landlock/sandboxer.c
> @@ -234,14 +234,16 @@ static bool check_ruleset_scope(const char *const env_var,
> bool error = false;
> bool abstract_scoping = false;
> bool signal_scoping = false;
> + bool named_scoping = false;
>
> /* Scoping is not supported by Landlock ABI */
> if (!(ruleset_attr->scoped &
> - (LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL)))
> + (LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL |
> + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET)))
> goto out_unset;
>
> env_type_scope = getenv(env_var);
> - /* Scoping is not supported by the user */
> + /* Scoping is not requested by the user */
> if (!env_type_scope || strcmp("", env_type_scope) == 0)
> goto out_unset;
>
> @@ -254,6 +256,9 @@ static bool check_ruleset_scope(const char *const env_var,
> } else if (strcmp("s", ipc_scoping_name) == 0 &&
> !signal_scoping) {
> signal_scoping = true;
> + } else if (strcmp("u", ipc_scoping_name) == 0 &&
> + !named_scoping) {
> + named_scoping = true;
> } else {
> fprintf(stderr, "Unknown or duplicate scope \"%s\"\n",
> ipc_scoping_name);
> @@ -270,6 +275,8 @@ static bool check_ruleset_scope(const char *const env_var,
> ruleset_attr->scoped &= ~LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET;
> if (!signal_scoping)
> ruleset_attr->scoped &= ~LANDLOCK_SCOPE_SIGNAL;
> + if (!named_scoping)
> + ruleset_attr->scoped &= ~LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
>
> unsetenv(env_var);
> return error;
> @@ -299,7 +306,7 @@ static bool check_ruleset_scope(const char *const env_var,
>
> /* clang-format on */
>
> -#define LANDLOCK_ABI_LAST 7
> +#define LANDLOCK_ABI_LAST 8
>
> #define XSTR(s) #s
> #define STR(s) XSTR(s)
> @@ -325,6 +332,7 @@ static const char help[] =
> "* " ENV_SCOPED_NAME ": actions denied on the outside of the landlock domain\n"
> " - \"a\" to restrict opening abstract unix sockets\n"
> " - \"s\" to restrict sending signals\n"
> + " - \"u\" to restrict opening pathname (non-abstract) unix sockets\n"
> "\n"
> "A sandboxer should not log denied access requests to avoid spamming logs, "
> "but to test audit we can set " ENV_FORCE_LOG_NAME "=1\n"
> @@ -334,7 +342,7 @@ static const char help[] =
> ENV_FS_RW_NAME "=\"/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp\" "
> ENV_TCP_BIND_NAME "=\"9418\" "
> ENV_TCP_CONNECT_NAME "=\"80:443\" "
> - ENV_SCOPED_NAME "=\"a:s\" "
> + ENV_SCOPED_NAME "=\"a:s:u\" "
> "%1$s bash -i\n"
> "\n"
> "This sandboxer can use Landlock features up to ABI version "
> @@ -356,7 +364,8 @@ int main(const int argc, char *const argv[], char *const *const envp)
> .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
> LANDLOCK_ACCESS_NET_CONNECT_TCP,
> .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
> - LANDLOCK_SCOPE_SIGNAL,
> + LANDLOCK_SCOPE_SIGNAL |
> + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET,
> };
> int supported_restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
> int set_restrict_flags = 0;
> @@ -436,6 +445,10 @@ int main(const int argc, char *const argv[], char *const *const envp)
> /* Removes LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON for ABI < 7 */
> supported_restrict_flags &=
> ~LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
> + __attribute__((fallthrough));
> + case 7:
> + /* Removes LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET for ABI < 8 */
> + ruleset_attr.scoped &= ~LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
>
> /* Must be printed for any ABI < LANDLOCK_ABI_LAST. */
> fprintf(stderr,
> --
> 2.52.0
>
^ permalink raw reply
* Re: [PATCH v2 5/6] selftests/landlock: Repurpose scoped_abstract_unix_test.c for pathname sockets too.
From: Mickaël Salaün @ 2026-01-29 21:28 UTC (permalink / raw)
To: Tingmao Wang
Cc: Günther Noack, Demi Marie Obenour, Alyssa Ross, Jann Horn,
Tahera Fahimi, Justin Suess, linux-security-module
In-Reply-To: <53b9883648225d5a08e82d2636ab0b4fda003bc9.1767115163.git.m@maowtm.org>
Commit messages should fit in 72 columns. The subject can be a bit more
but we should avoid that, and it should not end with a dot.
On Tue, Dec 30, 2025 at 05:20:23PM +0000, Tingmao Wang wrote:
> Since there is very little difference between abstract and pathname
> sockets in terms of testing of the scoped access checks (the only
> difference is in which scope bit control which form of socket), it makes
> sense to reuse the existing test for both type of sockets. Therefore, we
> rename scoped_abstract_unix_test.c to scoped_unix_test.c and extend the
> scoped_domains test to test pathname (i.e. non-abstract) sockets too.
>
> Since we can't change the variant data of scoped_domains (as it is defined
> in the shared .h file), we do this by extracting the actual test code into
> a function, and call it from different test cases.
>
> Also extend scoped_audit (this time we can use variants) to test both
> abstract and pathname sockets. For pathname sockets, audit_log_lsm_data
> will produce path="..." (or hex if path contains control characters) with
> absolute paths from the dentry, so we need to construct the escaped regex
> for the real path like in fs_test.
>
> Signed-off-by: Tingmao Wang <m@maowtm.org>
> ---
> ...bstract_unix_test.c => scoped_unix_test.c} | 256 ++++++++++++++----
> 1 file changed, 206 insertions(+), 50 deletions(-)
> rename tools/testing/selftests/landlock/{scoped_abstract_unix_test.c => scoped_unix_test.c} (81%)
>
> diff --git a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c b/tools/testing/selftests/landlock/scoped_unix_test.c
> similarity index 81%
> rename from tools/testing/selftests/landlock/scoped_abstract_unix_test.c
> rename to tools/testing/selftests/landlock/scoped_unix_test.c
> index 4a790e2d387d..669418c97509 100644
> --- a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c
> +++ b/tools/testing/selftests/landlock/scoped_unix_test.c
> @@ -1,6 +1,7 @@
> // SPDX-License-Identifier: GPL-2.0
> /*
> - * Landlock tests - Abstract UNIX socket
> + * Landlock tests - Scoped access checks for UNIX socket (abstract and
> + * pathname)
> *
> * Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com>
> */
> @@ -19,6 +20,7 @@
> #include <sys/un.h>
> #include <sys/wait.h>
> #include <unistd.h>
> +#include <stdlib.h>
>
> #include "audit.h"
> #include "common.h"
> @@ -47,7 +49,8 @@ static void create_fs_domain(struct __test_metadata *const _metadata)
>
> FIXTURE(scoped_domains)
> {
> - struct service_fixture stream_address, dgram_address;
> + struct service_fixture stream_address_abstract, dgram_address_abstract,
> + stream_address_pathname, dgram_address_pathname;
> };
>
> #include "scoped_base_variants.h"
> @@ -56,27 +59,62 @@ FIXTURE_SETUP(scoped_domains)
> {
> drop_caps(_metadata);
>
> - memset(&self->stream_address, 0, sizeof(self->stream_address));
> - memset(&self->dgram_address, 0, sizeof(self->dgram_address));
> - set_unix_address(&self->stream_address, 0, true);
> - set_unix_address(&self->dgram_address, 1, true);
> + ASSERT_EQ(0, mkdir(PATHNAME_UNIX_SOCK_DIR, 0700));
> +
> + memset(&self->stream_address_abstract, 0,
> + sizeof(self->stream_address_abstract));
> + memset(&self->dgram_address_abstract, 0,
> + sizeof(self->dgram_address_abstract));
> + memset(&self->stream_address_pathname, 0,
> + sizeof(self->stream_address_pathname));
> + memset(&self->dgram_address_pathname, 0,
> + sizeof(self->dgram_address_pathname));
> + set_unix_address(&self->stream_address_abstract, 0, true);
> + set_unix_address(&self->dgram_address_abstract, 1, true);
> + set_unix_address(&self->stream_address_pathname, 0, false);
> + set_unix_address(&self->dgram_address_pathname, 1, false);
> +}
> +
> +/* Remove @path if it exists */
> +int remove_path(const char *path)
> +{
> + if (unlink(path) == -1) {
> + if (errno != ENOENT)
> + return -errno;
> + }
> + return 0;
> }
>
> FIXTURE_TEARDOWN(scoped_domains)
> {
> + EXPECT_EQ(0, remove_path(self->stream_address_pathname.unix_addr.sun_path));
> + EXPECT_EQ(0, remove_path(self->dgram_address_pathname.unix_addr.sun_path));
> + EXPECT_EQ(0, rmdir(PATHNAME_UNIX_SOCK_DIR));
> }
>
> /*
> * Test unix_stream_connect() and unix_may_send() for a child connecting to its
> * parent, when they have scoped domain or no domain.
> */
> -TEST_F(scoped_domains, connect_to_parent)
> +static void test_connect_to_parent(struct __test_metadata *const _metadata,
> + FIXTURE_DATA(scoped_domains) * self,
> + const FIXTURE_VARIANT(scoped_domains) *
> + variant,
> + const bool abstract)
> {
> pid_t child;
> bool can_connect_to_parent;
> int status;
> int pipe_parent[2];
> int stream_server, dgram_server;
> + const __u16 scope = abstract ? LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET :
> + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
> + const struct service_fixture *stream_address =
> + abstract ? &self->stream_address_abstract :
> + &self->stream_address_pathname;
> + const struct service_fixture *dgram_address =
> + abstract ? &self->dgram_address_abstract :
> + &self->dgram_address_pathname;
>
> /*
> * can_connect_to_parent is true if a child process can connect to its
> @@ -87,8 +125,7 @@ TEST_F(scoped_domains, connect_to_parent)
>
> ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
> if (variant->domain_both) {
> - create_scoped_domain(_metadata,
> - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
> if (!__test_passed(_metadata))
> return;
> }
> @@ -102,8 +139,7 @@ TEST_F(scoped_domains, connect_to_parent)
>
> EXPECT_EQ(0, close(pipe_parent[1]));
> if (variant->domain_child)
> - create_scoped_domain(
> - _metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
>
> stream_client = socket(AF_UNIX, SOCK_STREAM, 0);
> ASSERT_LE(0, stream_client);
> @@ -113,8 +149,8 @@ TEST_F(scoped_domains, connect_to_parent)
> /* Waits for the server. */
> ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
>
> - err = connect(stream_client, &self->stream_address.unix_addr,
> - self->stream_address.unix_addr_len);
> + err = connect(stream_client, &stream_address->unix_addr,
> + stream_address->unix_addr_len);
> if (can_connect_to_parent) {
> EXPECT_EQ(0, err);
> } else {
> @@ -123,8 +159,8 @@ TEST_F(scoped_domains, connect_to_parent)
> }
> EXPECT_EQ(0, close(stream_client));
>
> - err = connect(dgram_client, &self->dgram_address.unix_addr,
> - self->dgram_address.unix_addr_len);
> + err = connect(dgram_client, &dgram_address->unix_addr,
> + dgram_address->unix_addr_len);
> if (can_connect_to_parent) {
> EXPECT_EQ(0, err);
> } else {
> @@ -137,17 +173,16 @@ TEST_F(scoped_domains, connect_to_parent)
> }
> EXPECT_EQ(0, close(pipe_parent[0]));
> if (variant->domain_parent)
> - create_scoped_domain(_metadata,
> - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
>
> stream_server = socket(AF_UNIX, SOCK_STREAM, 0);
> ASSERT_LE(0, stream_server);
> dgram_server = socket(AF_UNIX, SOCK_DGRAM, 0);
> ASSERT_LE(0, dgram_server);
> - ASSERT_EQ(0, bind(stream_server, &self->stream_address.unix_addr,
> - self->stream_address.unix_addr_len));
> - ASSERT_EQ(0, bind(dgram_server, &self->dgram_address.unix_addr,
> - self->dgram_address.unix_addr_len));
> + ASSERT_EQ(0, bind(stream_server, &stream_address->unix_addr,
> + stream_address->unix_addr_len));
> + ASSERT_EQ(0, bind(dgram_server, &dgram_address->unix_addr,
> + dgram_address->unix_addr_len));
> ASSERT_EQ(0, listen(stream_server, backlog));
>
> /* Signals to child that the parent is listening. */
> @@ -166,7 +201,11 @@ TEST_F(scoped_domains, connect_to_parent)
> * Test unix_stream_connect() and unix_may_send() for a parent connecting to
> * its child, when they have scoped domain or no domain.
> */
> -TEST_F(scoped_domains, connect_to_child)
> +static void test_connect_to_child(struct __test_metadata *const _metadata,
> + FIXTURE_DATA(scoped_domains) * self,
> + const FIXTURE_VARIANT(scoped_domains) *
> + variant,
> + const bool abstract)
> {
> pid_t child;
> bool can_connect_to_child;
> @@ -174,6 +213,14 @@ TEST_F(scoped_domains, connect_to_child)
> int pipe_child[2], pipe_parent[2];
> char buf;
> int stream_client, dgram_client;
> + const __u16 scope = abstract ? LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET :
> + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
> + const struct service_fixture *stream_address =
> + abstract ? &self->stream_address_abstract :
> + &self->stream_address_pathname;
> + const struct service_fixture *dgram_address =
> + abstract ? &self->dgram_address_abstract :
> + &self->dgram_address_pathname;
>
> /*
> * can_connect_to_child is true if a parent process can connect to its
> @@ -185,8 +232,7 @@ TEST_F(scoped_domains, connect_to_child)
> ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
> ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
> if (variant->domain_both) {
> - create_scoped_domain(_metadata,
> - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
> if (!__test_passed(_metadata))
> return;
> }
> @@ -199,8 +245,7 @@ TEST_F(scoped_domains, connect_to_child)
> EXPECT_EQ(0, close(pipe_parent[1]));
> EXPECT_EQ(0, close(pipe_child[0]));
> if (variant->domain_child)
> - create_scoped_domain(
> - _metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
>
> /* Waits for the parent to be in a domain, if any. */
> ASSERT_EQ(1, read(pipe_parent[0], &buf, 1));
> @@ -209,11 +254,10 @@ TEST_F(scoped_domains, connect_to_child)
> ASSERT_LE(0, stream_server);
> dgram_server = socket(AF_UNIX, SOCK_DGRAM, 0);
> ASSERT_LE(0, dgram_server);
> - ASSERT_EQ(0,
> - bind(stream_server, &self->stream_address.unix_addr,
> - self->stream_address.unix_addr_len));
> - ASSERT_EQ(0, bind(dgram_server, &self->dgram_address.unix_addr,
> - self->dgram_address.unix_addr_len));
> + ASSERT_EQ(0, bind(stream_server, &stream_address->unix_addr,
> + stream_address->unix_addr_len));
> + ASSERT_EQ(0, bind(dgram_server, &dgram_address->unix_addr,
> + dgram_address->unix_addr_len));
> ASSERT_EQ(0, listen(stream_server, backlog));
>
> /* Signals to the parent that child is listening. */
> @@ -230,8 +274,7 @@ TEST_F(scoped_domains, connect_to_child)
> EXPECT_EQ(0, close(pipe_parent[0]));
>
> if (variant->domain_parent)
> - create_scoped_domain(_metadata,
> - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
>
> /* Signals that the parent is in a domain, if any. */
> ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
> @@ -243,11 +286,11 @@ TEST_F(scoped_domains, connect_to_child)
>
> /* Waits for the child to listen */
> ASSERT_EQ(1, read(pipe_child[0], &buf, 1));
> - err_stream = connect(stream_client, &self->stream_address.unix_addr,
> - self->stream_address.unix_addr_len);
> + err_stream = connect(stream_client, &stream_address->unix_addr,
> + stream_address->unix_addr_len);
> errno_stream = errno;
> - err_dgram = connect(dgram_client, &self->dgram_address.unix_addr,
> - self->dgram_address.unix_addr_len);
> + err_dgram = connect(dgram_client, &dgram_address->unix_addr,
> + dgram_address->unix_addr_len);
> errno_dgram = errno;
> if (can_connect_to_child) {
> EXPECT_EQ(0, err_stream);
> @@ -268,19 +311,79 @@ TEST_F(scoped_domains, connect_to_child)
> _metadata->exit_code = KSFT_FAIL;
> }
>
> +/*
> + * Test unix_stream_connect() and unix_may_send() for a child connecting to its
> + * parent, when they have scoped domain or no domain.
> + */
> +TEST_F(scoped_domains, abstract_connect_to_parent)
> +{
> + test_connect_to_parent(_metadata, self, variant, true);
> +}
> +
> +/*
> + * Test unix_stream_connect() and unix_may_send() for a parent connecting to
> + * its child, when they have scoped domain or no domain.
> + */
> +TEST_F(scoped_domains, abstract_connect_to_child)
> +{
> + test_connect_to_child(_metadata, self, variant, true);
> +}
> +
> +/*
> + * Test unix_stream_connect() and unix_may_send() for a child connecting to its
> + * parent with pathname sockets.
> + */
> +TEST_F(scoped_domains, pathname_connect_to_parent)
> +{
> + test_connect_to_parent(_metadata, self, variant, false);
> +}
> +
> +/*
> + * Test unix_stream_connect() and unix_may_send() for a parent connecting to
> + * its child with pathname sockets.
> + */
> +TEST_F(scoped_domains, pathname_connect_to_child)
> +{
> + test_connect_to_child(_metadata, self, variant, false);
> +}
> +
> FIXTURE(scoped_audit)
> {
> - struct service_fixture dgram_address;
> + struct service_fixture dgram_address_abstract, dgram_address_pathname;
> struct audit_filter audit_filter;
> int audit_fd;
> };
>
> +FIXTURE_VARIANT(scoped_audit)
> +{
> + const bool abstract_socket;
> +};
> +
> +// clang-format off
We always use /* */ comments. Ditto for all clang-format markups.
> +FIXTURE_VARIANT_ADD(scoped_audit, abstract_socket)
> +{
> + // clang-format on
> + .abstract_socket = true,
> +};
> +
> +// clang-format off
> +FIXTURE_VARIANT_ADD(scoped_audit, pathname_socket)
> +{
> + // clang-format on
> + .abstract_socket = false,
> +};
> +
> FIXTURE_SETUP(scoped_audit)
> {
> disable_caps(_metadata);
>
> - memset(&self->dgram_address, 0, sizeof(self->dgram_address));
> - set_unix_address(&self->dgram_address, 1, true);
> + ASSERT_EQ(0, mkdir(PATHNAME_UNIX_SOCK_DIR, 0700));
> + memset(&self->dgram_address_abstract, 0,
> + sizeof(self->dgram_address_abstract));
> + memset(&self->dgram_address_pathname, 0,
> + sizeof(self->dgram_address_pathname));
> + set_unix_address(&self->dgram_address_abstract, 1, true);
> + set_unix_address(&self->dgram_address_pathname, 1, false);
>
> set_cap(_metadata, CAP_AUDIT_CONTROL);
> self->audit_fd = audit_init_with_exe_filter(&self->audit_filter);
> @@ -291,6 +394,8 @@ FIXTURE_SETUP(scoped_audit)
> FIXTURE_TEARDOWN_PARENT(scoped_audit)
> {
> EXPECT_EQ(0, audit_cleanup(-1, NULL));
> + EXPECT_EQ(0, remove_path(self->dgram_address_pathname.unix_addr.sun_path));
> + EXPECT_EQ(0, rmdir(PATHNAME_UNIX_SOCK_DIR));
> }
>
> /* python -c 'print(b"\0selftests-landlock-abstract-unix-".hex().upper())' */
> @@ -308,6 +413,12 @@ TEST_F(scoped_audit, connect_to_child)
> char buf;
> int dgram_client;
> struct audit_records records;
> + struct service_fixture *const dgram_address =
> + variant->abstract_socket ? &self->dgram_address_abstract :
> + &self->dgram_address_pathname;
> + size_t log_match_remaining = 500;
const
Why this number? Could you please follow the same logic as in
matches_log_fs_extra()?
> + char log_match[log_match_remaining];
> + char *log_match_cursor = log_match;
>
> /* Makes sure there is no superfluous logged records. */
> EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
> @@ -330,8 +441,8 @@ TEST_F(scoped_audit, connect_to_child)
>
> dgram_server = socket(AF_UNIX, SOCK_DGRAM, 0);
> ASSERT_LE(0, dgram_server);
> - ASSERT_EQ(0, bind(dgram_server, &self->dgram_address.unix_addr,
> - self->dgram_address.unix_addr_len));
> + ASSERT_EQ(0, bind(dgram_server, &dgram_address->unix_addr,
> + dgram_address->unix_addr_len));
>
> /* Signals to the parent that child is listening. */
> ASSERT_EQ(1, write(pipe_child[1], ".", 1));
> @@ -345,7 +456,9 @@ TEST_F(scoped_audit, connect_to_child)
> EXPECT_EQ(0, close(pipe_child[1]));
> EXPECT_EQ(0, close(pipe_parent[0]));
>
> - create_scoped_domain(_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata,
> + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
> + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET);
>
> /* Signals that the parent is in a domain, if any. */
> ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
> @@ -355,19 +468,62 @@ TEST_F(scoped_audit, connect_to_child)
>
> /* Waits for the child to listen */
> ASSERT_EQ(1, read(pipe_child[0], &buf, 1));
> - err_dgram = connect(dgram_client, &self->dgram_address.unix_addr,
> - self->dgram_address.unix_addr_len);
> + err_dgram = connect(dgram_client, &dgram_address->unix_addr,
> + dgram_address->unix_addr_len);
> EXPECT_EQ(-1, err_dgram);
> EXPECT_EQ(EPERM, errno);
>
> - EXPECT_EQ(
> - 0,
> - audit_match_record(
> - self->audit_fd, AUDIT_LANDLOCK_ACCESS,
> + if (variant->abstract_socket) {
> + log_match_cursor = stpncpy(
> + log_match,
> REGEX_LANDLOCK_PREFIX
> " blockers=scope\\.abstract_unix_socket path=" ABSTRACT_SOCKET_PATH_PREFIX
> "[0-9A-F]\\+$",
> - NULL));
> + log_match_remaining);
> + log_match_remaining =
> + sizeof(log_match) - (log_match_cursor - log_match);
> + ASSERT_NE(0, log_match_remaining);
> + } else {
> + /*
> + * It is assumed that absolute_path does not contain control
> + * characters nor spaces, see audit_string_contains_control().
> + */
> + char *absolute_path =
const char *absolute_path
> + realpath(dgram_address->unix_addr.sun_path, NULL);
> +
> + EXPECT_NE(NULL, absolute_path)
> + {
> + TH_LOG("realpath() failed: %s", strerror(errno));
> + return;
> + }
> +
> + log_match_cursor =
> + stpncpy(log_match,
> + REGEX_LANDLOCK_PREFIX
> + " blockers=scope\\.pathname_unix_socket path=\"",
> + log_match_remaining);
> + log_match_remaining =
> + sizeof(log_match) - (log_match_cursor - log_match);
> + ASSERT_NE(0, log_match_remaining);
> + log_match_cursor = regex_escape(absolute_path, log_match_cursor,
> + log_match_remaining);
> + free(absolute_path);
> + if (log_match_cursor < 0) {
> + TH_LOG("regex_escape() failed (buffer too small)");
> + return;
> + }
> + log_match_remaining =
> + sizeof(log_match) - (log_match_cursor - log_match);
> + ASSERT_NE(0, log_match_remaining);
> + log_match_cursor =
> + stpncpy(log_match_cursor, "\"$", log_match_remaining);
> + log_match_remaining =
> + sizeof(log_match) - (log_match_cursor - log_match);
> + ASSERT_NE(0, log_match_remaining);
> + }
> +
> + EXPECT_EQ(0, audit_match_record(self->audit_fd, AUDIT_LANDLOCK_ACCESS,
> + log_match, NULL));
>
> ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
> EXPECT_EQ(0, close(dgram_client));
> --
> 2.52.0
>
>
^ permalink raw reply
* Re: [PATCH v2 6/6] selftests/landlock: Add pathname socket variants for more tests
From: Mickaël Salaün @ 2026-01-29 21:28 UTC (permalink / raw)
To: Tingmao Wang
Cc: Günther Noack, Demi Marie Obenour, Alyssa Ross, Jann Horn,
Tahera Fahimi, Justin Suess, linux-security-module
In-Reply-To: <88de5bed60b06ba97088d87803f7bb3dbcc9a808.1767115163.git.m@maowtm.org>
The whole series looks good, thanks!
On Tue, Dec 30, 2025 at 05:20:24PM +0000, Tingmao Wang wrote:
> While this produces a lot of change, it does allow us to "simultaneously"
> test both abstract and pathname UNIX sockets with reletively little code
> duplication, since they are really similar.
>
> Tests touched: scoped_vs_unscoped, outside_socket,
> various_address_sockets, datagram_sockets, self_connect.
>
> Signed-off-by: Tingmao Wang <m@maowtm.org>
> ---
> .../selftests/landlock/scoped_unix_test.c | 599 ++++++++++++------
> 1 file changed, 395 insertions(+), 204 deletions(-)
>
> diff --git a/tools/testing/selftests/landlock/scoped_unix_test.c b/tools/testing/selftests/landlock/scoped_unix_test.c
> index 669418c97509..6d1541f77dbe 100644
> --- a/tools/testing/selftests/landlock/scoped_unix_test.c
> +++ b/tools/testing/selftests/landlock/scoped_unix_test.c
> @@ -536,8 +536,12 @@ TEST_F(scoped_audit, connect_to_child)
>
> FIXTURE(scoped_vs_unscoped)
> {
> - struct service_fixture parent_stream_address, parent_dgram_address,
> - child_stream_address, child_dgram_address;
> + struct service_fixture parent_stream_address_abstract,
> + parent_dgram_address_abstract, child_stream_address_abstract,
> + child_dgram_address_abstract;
> + struct service_fixture parent_stream_address_pathname,
> + parent_dgram_address_pathname, child_stream_address_pathname,
> + child_dgram_address_pathname;
> };
>
> #include "scoped_multiple_domain_variants.h"
> @@ -546,35 +550,75 @@ FIXTURE_SETUP(scoped_vs_unscoped)
> {
> drop_caps(_metadata);
>
> - memset(&self->parent_stream_address, 0,
> - sizeof(self->parent_stream_address));
> - set_unix_address(&self->parent_stream_address, 0, true);
> - memset(&self->parent_dgram_address, 0,
> - sizeof(self->parent_dgram_address));
> - set_unix_address(&self->parent_dgram_address, 1, true);
> - memset(&self->child_stream_address, 0,
> - sizeof(self->child_stream_address));
> - set_unix_address(&self->child_stream_address, 2, true);
> - memset(&self->child_dgram_address, 0,
> - sizeof(self->child_dgram_address));
> - set_unix_address(&self->child_dgram_address, 3, true);
> + ASSERT_EQ(0, mkdir(PATHNAME_UNIX_SOCK_DIR, 0700));
> +
> + /* Abstract addresses. */
> + memset(&self->parent_stream_address_abstract, 0,
> + sizeof(self->parent_stream_address_abstract));
> + set_unix_address(&self->parent_stream_address_abstract, 0, true);
> + memset(&self->parent_dgram_address_abstract, 0,
> + sizeof(self->parent_dgram_address_abstract));
> + set_unix_address(&self->parent_dgram_address_abstract, 1, true);
> + memset(&self->child_stream_address_abstract, 0,
> + sizeof(self->child_stream_address_abstract));
> + set_unix_address(&self->child_stream_address_abstract, 2, true);
> + memset(&self->child_dgram_address_abstract, 0,
> + sizeof(self->child_dgram_address_abstract));
> + set_unix_address(&self->child_dgram_address_abstract, 3, true);
> +
> + /* Pathname addresses. */
> + memset(&self->parent_stream_address_pathname, 0,
> + sizeof(self->parent_stream_address_pathname));
> + set_unix_address(&self->parent_stream_address_pathname, 4, false);
> + memset(&self->parent_dgram_address_pathname, 0,
> + sizeof(self->parent_dgram_address_pathname));
> + set_unix_address(&self->parent_dgram_address_pathname, 5, false);
> + memset(&self->child_stream_address_pathname, 0,
> + sizeof(self->child_stream_address_pathname));
> + set_unix_address(&self->child_stream_address_pathname, 6, false);
> + memset(&self->child_dgram_address_pathname, 0,
> + sizeof(self->child_dgram_address_pathname));
> + set_unix_address(&self->child_dgram_address_pathname, 7, false);
> }
>
> FIXTURE_TEARDOWN(scoped_vs_unscoped)
> {
> + EXPECT_EQ(0, remove_path(self->parent_stream_address_pathname.unix_addr.sun_path));
> + EXPECT_EQ(0, remove_path(self->parent_dgram_address_pathname.unix_addr.sun_path));
> + EXPECT_EQ(0, remove_path(self->child_stream_address_pathname.unix_addr.sun_path));
> + EXPECT_EQ(0, remove_path(self->child_dgram_address_pathname.unix_addr.sun_path));
> + EXPECT_EQ(0, rmdir(PATHNAME_UNIX_SOCK_DIR));
> }
>
> /*
> * Test unix_stream_connect and unix_may_send for parent, child and
> * grand child processes when they can have scoped or non-scoped domains.
> */
> -TEST_F(scoped_vs_unscoped, unix_scoping)
> +static void test_scoped_vs_unscoped(
> + struct __test_metadata *const _metadata,
> + FIXTURE_DATA(scoped_vs_unscoped) * self,
> + const FIXTURE_VARIANT(scoped_vs_unscoped) * variant,
> + const bool abstract)
> {
> pid_t child;
> int status;
> bool can_connect_to_parent, can_connect_to_child;
> int pipe_parent[2];
> int stream_server_parent, dgram_server_parent;
> + const __u16 scope = abstract ? LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET :
> + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
> + const struct service_fixture *parent_stream_address =
> + abstract ? &self->parent_stream_address_abstract :
> + &self->parent_stream_address_pathname;
> + const struct service_fixture *parent_dgram_address =
> + abstract ? &self->parent_dgram_address_abstract :
> + &self->parent_dgram_address_pathname;
> + const struct service_fixture *child_stream_address =
> + abstract ? &self->child_stream_address_abstract :
> + &self->child_stream_address_pathname;
> + const struct service_fixture *child_dgram_address =
> + abstract ? &self->child_dgram_address_abstract :
> + &self->child_dgram_address_pathname;
>
> can_connect_to_child = (variant->domain_grand_child != SCOPE_SANDBOX);
> can_connect_to_parent = (can_connect_to_child &&
> @@ -585,8 +629,7 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
> if (variant->domain_all == OTHER_SANDBOX)
> create_fs_domain(_metadata);
> else if (variant->domain_all == SCOPE_SANDBOX)
> - create_scoped_domain(_metadata,
> - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
>
> child = fork();
> ASSERT_LE(0, child);
> @@ -600,8 +643,7 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
> if (variant->domain_children == OTHER_SANDBOX)
> create_fs_domain(_metadata);
> else if (variant->domain_children == SCOPE_SANDBOX)
> - create_scoped_domain(
> - _metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
>
> grand_child = fork();
> ASSERT_LE(0, grand_child);
> @@ -616,9 +658,7 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
> if (variant->domain_grand_child == OTHER_SANDBOX)
> create_fs_domain(_metadata);
> else if (variant->domain_grand_child == SCOPE_SANDBOX)
> - create_scoped_domain(
> - _metadata,
> - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
>
> stream_client = socket(AF_UNIX, SOCK_STREAM, 0);
> ASSERT_LE(0, stream_client);
> @@ -626,15 +666,13 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
> ASSERT_LE(0, dgram_client);
>
> ASSERT_EQ(1, read(pipe_child[0], &buf, 1));
> - stream_err = connect(
> - stream_client,
> - &self->child_stream_address.unix_addr,
> - self->child_stream_address.unix_addr_len);
> + stream_err = connect(stream_client,
> + &child_stream_address->unix_addr,
> + child_stream_address->unix_addr_len);
> stream_errno = errno;
> - dgram_err = connect(
> - dgram_client,
> - &self->child_dgram_address.unix_addr,
> - self->child_dgram_address.unix_addr_len);
> + dgram_err = connect(dgram_client,
> + &child_dgram_address->unix_addr,
> + child_dgram_address->unix_addr_len);
> dgram_errno = errno;
> if (can_connect_to_child) {
> EXPECT_EQ(0, stream_err);
> @@ -653,14 +691,12 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
>
> ASSERT_EQ(1, read(pipe_parent[0], &buf, 1));
> stream_err = connect(
> - stream_client,
> - &self->parent_stream_address.unix_addr,
> - self->parent_stream_address.unix_addr_len);
> + stream_client, &parent_stream_address->unix_addr,
> + parent_stream_address->unix_addr_len);
> stream_errno = errno;
> dgram_err = connect(
> - dgram_client,
> - &self->parent_dgram_address.unix_addr,
> - self->parent_dgram_address.unix_addr_len);
> + dgram_client, &parent_dgram_address->unix_addr,
> + parent_dgram_address->unix_addr_len);
> dgram_errno = errno;
> if (can_connect_to_parent) {
> EXPECT_EQ(0, stream_err);
> @@ -681,8 +717,7 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
> if (variant->domain_child == OTHER_SANDBOX)
> create_fs_domain(_metadata);
> else if (variant->domain_child == SCOPE_SANDBOX)
> - create_scoped_domain(
> - _metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
>
> stream_server_child = socket(AF_UNIX, SOCK_STREAM, 0);
> ASSERT_LE(0, stream_server_child);
> @@ -690,11 +725,11 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
> ASSERT_LE(0, dgram_server_child);
>
> ASSERT_EQ(0, bind(stream_server_child,
> - &self->child_stream_address.unix_addr,
> - self->child_stream_address.unix_addr_len));
> - ASSERT_EQ(0, bind(dgram_server_child,
> - &self->child_dgram_address.unix_addr,
> - self->child_dgram_address.unix_addr_len));
> + &child_stream_address->unix_addr,
> + child_stream_address->unix_addr_len));
> + ASSERT_EQ(0,
> + bind(dgram_server_child, &child_dgram_address->unix_addr,
> + child_dgram_address->unix_addr_len));
> ASSERT_EQ(0, listen(stream_server_child, backlog));
>
> ASSERT_EQ(1, write(pipe_child[1], ".", 1));
> @@ -708,19 +743,16 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
> if (variant->domain_parent == OTHER_SANDBOX)
> create_fs_domain(_metadata);
> else if (variant->domain_parent == SCOPE_SANDBOX)
> - create_scoped_domain(_metadata,
> - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
>
> stream_server_parent = socket(AF_UNIX, SOCK_STREAM, 0);
> ASSERT_LE(0, stream_server_parent);
> dgram_server_parent = socket(AF_UNIX, SOCK_DGRAM, 0);
> ASSERT_LE(0, dgram_server_parent);
> - ASSERT_EQ(0, bind(stream_server_parent,
> - &self->parent_stream_address.unix_addr,
> - self->parent_stream_address.unix_addr_len));
> - ASSERT_EQ(0, bind(dgram_server_parent,
> - &self->parent_dgram_address.unix_addr,
> - self->parent_dgram_address.unix_addr_len));
> + ASSERT_EQ(0, bind(stream_server_parent, &parent_stream_address->unix_addr,
> + parent_stream_address->unix_addr_len));
> + ASSERT_EQ(0, bind(dgram_server_parent, &parent_dgram_address->unix_addr,
> + parent_dgram_address->unix_addr_len));
>
> ASSERT_EQ(0, listen(stream_server_parent, backlog));
>
> @@ -734,57 +766,119 @@ TEST_F(scoped_vs_unscoped, unix_scoping)
> _metadata->exit_code = KSFT_FAIL;
> }
>
> +TEST_F(scoped_vs_unscoped, unix_scoping_abstract)
> +{
> + test_scoped_vs_unscoped(_metadata, self, variant, true);
> +}
> +
> +TEST_F(scoped_vs_unscoped, unix_scoping_pathname)
> +{
> + test_scoped_vs_unscoped(_metadata, self, variant, false);
> +}
> +
> FIXTURE(outside_socket)
> {
> - struct service_fixture address, transit_address;
> + struct service_fixture address_abstract, transit_address_abstract;
> + struct service_fixture address_pathname, transit_address_pathname;
> };
>
> FIXTURE_VARIANT(outside_socket)
> {
> const bool child_socket;
> const int type;
> + const bool abstract;
> +};
> +
> +/* clang-format off */
> +FIXTURE_VARIANT_ADD(outside_socket, abstract_allow_dgram_child) {
> + /* clang-format on */
> + .child_socket = true,
> + .type = SOCK_DGRAM,
> + .abstract = true,
> +};
> +
> +/* clang-format off */
> +FIXTURE_VARIANT_ADD(outside_socket, abstract_deny_dgram_server) {
> + /* clang-format on */
> + .child_socket = false,
> + .type = SOCK_DGRAM,
> + .abstract = true,
> +};
> +
> +/* clang-format off */
> +FIXTURE_VARIANT_ADD(outside_socket, abstract_allow_stream_child) {
> + /* clang-format on */
> + .child_socket = true,
> + .type = SOCK_STREAM,
> + .abstract = true,
> };
>
> /* clang-format off */
> -FIXTURE_VARIANT_ADD(outside_socket, allow_dgram_child) {
> +FIXTURE_VARIANT_ADD(outside_socket, abstract_deny_stream_server) {
> + /* clang-format on */
> + .child_socket = false,
> + .type = SOCK_STREAM,
> + .abstract = true,
> +};
> +
> +/* clang-format off */
> +FIXTURE_VARIANT_ADD(outside_socket, pathname_allow_dgram_child) {
> /* clang-format on */
> .child_socket = true,
> .type = SOCK_DGRAM,
> + .abstract = false,
> };
>
> /* clang-format off */
> -FIXTURE_VARIANT_ADD(outside_socket, deny_dgram_server) {
> +FIXTURE_VARIANT_ADD(outside_socket, pathname_deny_dgram_server) {
> /* clang-format on */
> .child_socket = false,
> .type = SOCK_DGRAM,
> + .abstract = false,
> };
>
> /* clang-format off */
> -FIXTURE_VARIANT_ADD(outside_socket, allow_stream_child) {
> +FIXTURE_VARIANT_ADD(outside_socket, pathname_allow_stream_child) {
> /* clang-format on */
> .child_socket = true,
> .type = SOCK_STREAM,
> + .abstract = false,
> };
>
> /* clang-format off */
> -FIXTURE_VARIANT_ADD(outside_socket, deny_stream_server) {
> +FIXTURE_VARIANT_ADD(outside_socket, pathname_deny_stream_server) {
> /* clang-format on */
> .child_socket = false,
> .type = SOCK_STREAM,
> + .abstract = false,
> };
>
> FIXTURE_SETUP(outside_socket)
> {
> drop_caps(_metadata);
>
> - memset(&self->transit_address, 0, sizeof(self->transit_address));
> - set_unix_address(&self->transit_address, 0, true);
> - memset(&self->address, 0, sizeof(self->address));
> - set_unix_address(&self->address, 1, true);
> + ASSERT_EQ(0, mkdir(PATHNAME_UNIX_SOCK_DIR, 0700));
> +
> + /* Abstract addresses. */
> + memset(&self->transit_address_abstract, 0,
> + sizeof(self->transit_address_abstract));
> + set_unix_address(&self->transit_address_abstract, 0, true);
> + memset(&self->address_abstract, 0, sizeof(self->address_abstract));
> + set_unix_address(&self->address_abstract, 1, true);
> +
> + /* Pathname addresses. */
> + memset(&self->transit_address_pathname, 0,
> + sizeof(self->transit_address_pathname));
> + set_unix_address(&self->transit_address_pathname, 2, false);
> + memset(&self->address_pathname, 0, sizeof(self->address_pathname));
> + set_unix_address(&self->address_pathname, 3, false);
> }
>
> FIXTURE_TEARDOWN(outside_socket)
> {
> + EXPECT_EQ(0, remove_path(self->transit_address_pathname.unix_addr.sun_path));
> + EXPECT_EQ(0, remove_path(self->address_pathname.unix_addr.sun_path));
> + EXPECT_EQ(0, rmdir(PATHNAME_UNIX_SOCK_DIR));
> }
>
> /*
> @@ -798,6 +892,15 @@ TEST_F(outside_socket, socket_with_different_domain)
> int pipe_child[2], pipe_parent[2];
> char buf_parent;
> int server_socket;
> + const __u16 scope = variant->abstract ?
> + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET :
> + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
> + const struct service_fixture *transit_address =
> + variant->abstract ? &self->transit_address_abstract :
> + &self->transit_address_pathname;
> + const struct service_fixture *address =
> + variant->abstract ? &self->address_abstract :
> + &self->address_pathname;
>
> ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
> ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
> @@ -812,8 +915,7 @@ TEST_F(outside_socket, socket_with_different_domain)
> EXPECT_EQ(0, close(pipe_child[0]));
>
> /* Client always has a domain. */
> - create_scoped_domain(_metadata,
> - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
>
> if (variant->child_socket) {
> int data_socket, passed_socket, stream_server;
> @@ -823,8 +925,8 @@ TEST_F(outside_socket, socket_with_different_domain)
> stream_server = socket(AF_UNIX, SOCK_STREAM, 0);
> ASSERT_LE(0, stream_server);
> ASSERT_EQ(0, bind(stream_server,
> - &self->transit_address.unix_addr,
> - self->transit_address.unix_addr_len));
> + &transit_address->unix_addr,
> + transit_address->unix_addr_len));
> ASSERT_EQ(0, listen(stream_server, backlog));
> ASSERT_EQ(1, write(pipe_child[1], ".", 1));
> data_socket = accept(stream_server, NULL, NULL);
> @@ -839,8 +941,8 @@ TEST_F(outside_socket, socket_with_different_domain)
>
> /* Waits for parent signal for connection. */
> ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
> - err = connect(client_socket, &self->address.unix_addr,
> - self->address.unix_addr_len);
> + err = connect(client_socket, &address->unix_addr,
> + address->unix_addr_len);
> if (variant->child_socket) {
> EXPECT_EQ(0, err);
> } else {
> @@ -859,9 +961,8 @@ TEST_F(outside_socket, socket_with_different_domain)
>
> ASSERT_LE(0, client_child);
> ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1));
> - ASSERT_EQ(0, connect(client_child,
> - &self->transit_address.unix_addr,
> - self->transit_address.unix_addr_len));
> + ASSERT_EQ(0, connect(client_child, &transit_address->unix_addr,
> + transit_address->unix_addr_len));
> server_socket = recv_fd(client_child);
> EXPECT_EQ(0, close(client_child));
> } else {
> @@ -870,10 +971,10 @@ TEST_F(outside_socket, socket_with_different_domain)
> ASSERT_LE(0, server_socket);
>
> /* Server always has a domain. */
> - create_scoped_domain(_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
>
> - ASSERT_EQ(0, bind(server_socket, &self->address.unix_addr,
> - self->address.unix_addr_len));
> + ASSERT_EQ(0,
> + bind(server_socket, &address->unix_addr, address->unix_addr_len));
> if (variant->type == SOCK_STREAM)
> ASSERT_EQ(0, listen(server_socket, backlog));
>
> @@ -888,52 +989,85 @@ TEST_F(outside_socket, socket_with_different_domain)
> _metadata->exit_code = KSFT_FAIL;
> }
>
> -static const char stream_path[] = TMP_DIR "/stream.sock";
> -static const char dgram_path[] = TMP_DIR "/dgram.sock";
> -
> /* clang-format off */
> -FIXTURE(various_address_sockets) {};
> +FIXTURE(various_address_sockets) {
> + struct service_fixture stream_pathname_addr, dgram_pathname_addr;
> + struct service_fixture stream_abstract_addr, dgram_abstract_addr;
> +};
> /* clang-format on */
>
> -FIXTURE_VARIANT(various_address_sockets)
> -{
> - const int domain;
> +/*
> + * Test all 4 combinations of abstract and pathname socket scope bits,
> + * plus a case with no Landlock domain at all.
> + */
> +/* clang-format off */
> +FIXTURE_VARIANT(various_address_sockets) {
> + /* clang-format on */
> + const __u16 scope_bits;
> + const bool no_sandbox;
> +};
> +
> +/* clang-format off */
> +FIXTURE_VARIANT_ADD(various_address_sockets, scope_abstract) {
> + /* clang-format on */
> + .scope_bits = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
> +};
> +
> +/* clang-format off */
> +FIXTURE_VARIANT_ADD(various_address_sockets, scope_pathname) {
> + /* clang-format on */
> + .scope_bits = LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET,
> };
>
> /* clang-format off */
> -FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_scoped_domain) {
> +FIXTURE_VARIANT_ADD(various_address_sockets, scope_both) {
> /* clang-format on */
> - .domain = SCOPE_SANDBOX,
> + .scope_bits = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
> + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET,
> };
>
> /* clang-format off */
> -FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_other_domain) {
> +FIXTURE_VARIANT_ADD(various_address_sockets, scope_none) {
> /* clang-format on */
> - .domain = OTHER_SANDBOX,
> + .scope_bits = 0,
> };
>
> /* clang-format off */
> -FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_no_domain) {
> +FIXTURE_VARIANT_ADD(various_address_sockets, no_domain) {
> /* clang-format on */
> - .domain = NO_SANDBOX,
> + .no_sandbox = true,
> };
>
> FIXTURE_SETUP(various_address_sockets)
> {
> drop_caps(_metadata);
>
> - umask(0077);
> - ASSERT_EQ(0, mkdir(TMP_DIR, 0700));
> + ASSERT_EQ(0, mkdir(PATHNAME_UNIX_SOCK_DIR, 0700));
> +
> + memset(&self->stream_pathname_addr, 0, sizeof(self->stream_pathname_addr));
> + set_unix_address(&self->stream_pathname_addr, 0, false);
> + memset(&self->dgram_pathname_addr, 0, sizeof(self->dgram_pathname_addr));
> + set_unix_address(&self->dgram_pathname_addr, 1, false);
> +
> + memset(&self->stream_abstract_addr, 0, sizeof(self->stream_abstract_addr));
> + set_unix_address(&self->stream_abstract_addr, 2, true);
> + memset(&self->dgram_abstract_addr, 0, sizeof(self->dgram_abstract_addr));
> + set_unix_address(&self->dgram_abstract_addr, 3, true);
> }
>
> FIXTURE_TEARDOWN(various_address_sockets)
> {
> - EXPECT_EQ(0, unlink(stream_path));
> - EXPECT_EQ(0, unlink(dgram_path));
> - EXPECT_EQ(0, rmdir(TMP_DIR));
> + EXPECT_EQ(0, remove_path(self->stream_pathname_addr.unix_addr.sun_path));
> + EXPECT_EQ(0, remove_path(self->dgram_pathname_addr.unix_addr.sun_path));
> + EXPECT_EQ(0, rmdir(PATHNAME_UNIX_SOCK_DIR));
> }
>
> -TEST_F(various_address_sockets, scoped_pathname_sockets)
> +/*
> + * Test interaction of various scope flags (controlled by variant->domain)
> + * with pathname and abstract sockets when connecting from a sandboxed
> + * child.
> + */
> +TEST_F(various_address_sockets, scoped_sockets)
> {
> pid_t child;
> int status;
> @@ -942,25 +1076,10 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)
> int unnamed_sockets[2];
> int stream_pathname_socket, dgram_pathname_socket,
> stream_abstract_socket, dgram_abstract_socket, data_socket;
> - struct service_fixture stream_abstract_addr, dgram_abstract_addr;
> - struct sockaddr_un stream_pathname_addr = {
> - .sun_family = AF_UNIX,
> - };
> - struct sockaddr_un dgram_pathname_addr = {
> - .sun_family = AF_UNIX,
> - };
> -
> - /* Pathname address. */
> - snprintf(stream_pathname_addr.sun_path,
> - sizeof(stream_pathname_addr.sun_path), "%s", stream_path);
> - snprintf(dgram_pathname_addr.sun_path,
> - sizeof(dgram_pathname_addr.sun_path), "%s", dgram_path);
> -
> - /* Abstract address. */
> - memset(&stream_abstract_addr, 0, sizeof(stream_abstract_addr));
> - set_unix_address(&stream_abstract_addr, 0, true);
> - memset(&dgram_abstract_addr, 0, sizeof(dgram_abstract_addr));
> - set_unix_address(&dgram_abstract_addr, 1, true);
> + bool pathname_restricted =
> + (variant->scope_bits & LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET);
> + bool abstract_restricted =
> + (variant->scope_bits & LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
>
> /* Unnamed address for datagram socket. */
> ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_DGRAM, 0, unnamed_sockets));
> @@ -975,82 +1094,103 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)
> EXPECT_EQ(0, close(pipe_parent[1]));
> EXPECT_EQ(0, close(unnamed_sockets[1]));
>
> - if (variant->domain == SCOPE_SANDBOX)
> - create_scoped_domain(
> - _metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> - else if (variant->domain == OTHER_SANDBOX)
> + /* Create domain based on variant. */
> + if (variant->scope_bits)
> + create_scoped_domain(_metadata, variant->scope_bits);
> + else if (!variant->no_sandbox)
> create_fs_domain(_metadata);
>
> /* Waits for parent to listen. */
> ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
> EXPECT_EQ(0, close(pipe_parent[0]));
>
> - /* Checks that we can send data through a datagram socket. */
> + /* Checks that we can send data through a unnamed socket. */
an unnamed
> ASSERT_EQ(1, write(unnamed_sockets[0], "a", 1));
> EXPECT_EQ(0, close(unnamed_sockets[0]));
>
> /* Connects with pathname sockets. */
> stream_pathname_socket = socket(AF_UNIX, SOCK_STREAM, 0);
> ASSERT_LE(0, stream_pathname_socket);
> - ASSERT_EQ(0,
> - connect(stream_pathname_socket, &stream_pathname_addr,
> - sizeof(stream_pathname_addr)));
> - ASSERT_EQ(1, write(stream_pathname_socket, "b", 1));
> + err = connect(stream_pathname_socket,
> + &self->stream_pathname_addr.unix_addr,
> + self->stream_pathname_addr.unix_addr_len);
> + if (pathname_restricted) {
> + EXPECT_EQ(-1, err);
> + EXPECT_EQ(EPERM, errno);
> + } else {
> + EXPECT_EQ(0, err);
> + ASSERT_EQ(1, write(stream_pathname_socket, "b", 1));
> + }
> EXPECT_EQ(0, close(stream_pathname_socket));
>
> - /* Sends without connection. */
> + /* Sends without connection (pathname). */
> dgram_pathname_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
> ASSERT_LE(0, dgram_pathname_socket);
> err = sendto(dgram_pathname_socket, "c", 1, 0,
> - &dgram_pathname_addr, sizeof(dgram_pathname_addr));
> - EXPECT_EQ(1, err);
> + &self->dgram_pathname_addr.unix_addr,
> + self->dgram_pathname_addr.unix_addr_len);
> + if (pathname_restricted) {
> + EXPECT_EQ(-1, err);
> + EXPECT_EQ(EPERM, errno);
> + } else {
> + EXPECT_EQ(1, err);
> + }
> +
> + /* Sends with connection (pathname). */
> + err = connect(dgram_pathname_socket,
> + &self->dgram_pathname_addr.unix_addr,
> + self->dgram_pathname_addr.unix_addr_len);
> + if (pathname_restricted) {
> + EXPECT_EQ(-1, err);
> + EXPECT_EQ(EPERM, errno);
> + } else {
> + EXPECT_EQ(0, err);
> + ASSERT_EQ(1, write(dgram_pathname_socket, "d", 1));
> + }
>
> - /* Sends with connection. */
> - ASSERT_EQ(0,
> - connect(dgram_pathname_socket, &dgram_pathname_addr,
> - sizeof(dgram_pathname_addr)));
> - ASSERT_EQ(1, write(dgram_pathname_socket, "d", 1));
> EXPECT_EQ(0, close(dgram_pathname_socket));
>
> /* Connects with abstract sockets. */
> stream_abstract_socket = socket(AF_UNIX, SOCK_STREAM, 0);
> ASSERT_LE(0, stream_abstract_socket);
> err = connect(stream_abstract_socket,
> - &stream_abstract_addr.unix_addr,
> - stream_abstract_addr.unix_addr_len);
> - if (variant->domain == SCOPE_SANDBOX) {
> + &self->stream_abstract_addr.unix_addr,
> + self->stream_abstract_addr.unix_addr_len);
> + if (abstract_restricted) {
> EXPECT_EQ(-1, err);
> EXPECT_EQ(EPERM, errno);
> } else {
> EXPECT_EQ(0, err);
> ASSERT_EQ(1, write(stream_abstract_socket, "e", 1));
> }
> +
> EXPECT_EQ(0, close(stream_abstract_socket));
>
> - /* Sends without connection. */
> + /* Sends without connection (abstract). */
> dgram_abstract_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
> ASSERT_LE(0, dgram_abstract_socket);
> err = sendto(dgram_abstract_socket, "f", 1, 0,
> - &dgram_abstract_addr.unix_addr,
> - dgram_abstract_addr.unix_addr_len);
> - if (variant->domain == SCOPE_SANDBOX) {
> + &self->dgram_abstract_addr.unix_addr,
> + self->dgram_abstract_addr.unix_addr_len);
> + if (abstract_restricted) {
> EXPECT_EQ(-1, err);
> EXPECT_EQ(EPERM, errno);
> } else {
> EXPECT_EQ(1, err);
> }
>
> - /* Sends with connection. */
> + /* Sends with connection (abstract). */
> err = connect(dgram_abstract_socket,
> - &dgram_abstract_addr.unix_addr,
> - dgram_abstract_addr.unix_addr_len);
> - if (variant->domain == SCOPE_SANDBOX) {
> + &self->dgram_abstract_addr.unix_addr,
> + self->dgram_abstract_addr.unix_addr_len);
> + if (abstract_restricted) {
> EXPECT_EQ(-1, err);
> EXPECT_EQ(EPERM, errno);
> } else {
> EXPECT_EQ(0, err);
> ASSERT_EQ(1, write(dgram_abstract_socket, "g", 1));
> }
> +
> EXPECT_EQ(0, close(dgram_abstract_socket));
>
> _exit(_metadata->exit_code);
> @@ -1062,27 +1202,30 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)
> /* Sets up pathname servers. */
> stream_pathname_socket = socket(AF_UNIX, SOCK_STREAM, 0);
> ASSERT_LE(0, stream_pathname_socket);
> - ASSERT_EQ(0, bind(stream_pathname_socket, &stream_pathname_addr,
> - sizeof(stream_pathname_addr)));
> + ASSERT_EQ(0, bind(stream_pathname_socket,
> + &self->stream_pathname_addr.unix_addr,
> + self->stream_pathname_addr.unix_addr_len));
> ASSERT_EQ(0, listen(stream_pathname_socket, backlog));
>
> dgram_pathname_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
> ASSERT_LE(0, dgram_pathname_socket);
> - ASSERT_EQ(0, bind(dgram_pathname_socket, &dgram_pathname_addr,
> - sizeof(dgram_pathname_addr)));
> + ASSERT_EQ(0, bind(dgram_pathname_socket,
> + &self->dgram_pathname_addr.unix_addr,
> + self->dgram_pathname_addr.unix_addr_len));
>
> /* Sets up abstract servers. */
> stream_abstract_socket = socket(AF_UNIX, SOCK_STREAM, 0);
> ASSERT_LE(0, stream_abstract_socket);
> - ASSERT_EQ(0,
> - bind(stream_abstract_socket, &stream_abstract_addr.unix_addr,
> - stream_abstract_addr.unix_addr_len));
> + ASSERT_EQ(0, bind(stream_abstract_socket,
> + &self->stream_abstract_addr.unix_addr,
> + self->stream_abstract_addr.unix_addr_len));
> + ASSERT_EQ(0, listen(stream_abstract_socket, backlog));
>
> dgram_abstract_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
> ASSERT_LE(0, dgram_abstract_socket);
> - ASSERT_EQ(0, bind(dgram_abstract_socket, &dgram_abstract_addr.unix_addr,
> - dgram_abstract_addr.unix_addr_len));
> - ASSERT_EQ(0, listen(stream_abstract_socket, backlog));
> + ASSERT_EQ(0, bind(dgram_abstract_socket,
> + &self->dgram_abstract_addr.unix_addr,
> + self->dgram_abstract_addr.unix_addr_len));
>
> ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
> EXPECT_EQ(0, close(pipe_parent[1]));
> @@ -1092,24 +1235,31 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)
> ASSERT_EQ('a', buf_parent);
> EXPECT_LE(0, close(unnamed_sockets[1]));
>
> - /* Reads from pathname sockets. */
> - data_socket = accept(stream_pathname_socket, NULL, NULL);
> - ASSERT_LE(0, data_socket);
> - ASSERT_EQ(1, read(data_socket, &buf_parent, sizeof(buf_parent)));
> - ASSERT_EQ('b', buf_parent);
> - EXPECT_EQ(0, close(data_socket));
> - EXPECT_EQ(0, close(stream_pathname_socket));
> + if (!pathname_restricted) {
> + /*
> + * Reads from pathname sockets if we expect child to be able to
> + * send.
> + */
> + data_socket = accept(stream_pathname_socket, NULL, NULL);
> + ASSERT_LE(0, data_socket);
> + ASSERT_EQ(1,
> + read(data_socket, &buf_parent, sizeof(buf_parent)));
> + ASSERT_EQ('b', buf_parent);
> + EXPECT_EQ(0, close(data_socket));
>
> - ASSERT_EQ(1,
> - read(dgram_pathname_socket, &buf_parent, sizeof(buf_parent)));
> - ASSERT_EQ('c', buf_parent);
> - ASSERT_EQ(1,
> - read(dgram_pathname_socket, &buf_parent, sizeof(buf_parent)));
> - ASSERT_EQ('d', buf_parent);
> - EXPECT_EQ(0, close(dgram_pathname_socket));
> + ASSERT_EQ(1, read(dgram_pathname_socket, &buf_parent,
> + sizeof(buf_parent)));
> + ASSERT_EQ('c', buf_parent);
> + ASSERT_EQ(1, read(dgram_pathname_socket, &buf_parent,
> + sizeof(buf_parent)));
> + ASSERT_EQ('d', buf_parent);
> + }
>
> - if (variant->domain != SCOPE_SANDBOX) {
> - /* Reads from abstract sockets if allowed to send. */
> + if (!abstract_restricted) {
> + /*
> + * Reads from abstract sockets if we expect child to be able to
> + * send.
> + */
> data_socket = accept(stream_abstract_socket, NULL, NULL);
> ASSERT_LE(0, data_socket);
> ASSERT_EQ(1,
> @@ -1125,30 +1275,73 @@ TEST_F(various_address_sockets, scoped_pathname_sockets)
> ASSERT_EQ('g', buf_parent);
> }
>
> - /* Waits for all abstract socket tests. */
> + /* Waits for child to complete, and only close the socket afterwards. */
> ASSERT_EQ(child, waitpid(child, &status, 0));
> EXPECT_EQ(0, close(stream_abstract_socket));
> EXPECT_EQ(0, close(dgram_abstract_socket));
> + EXPECT_EQ(0, close(stream_pathname_socket));
> + EXPECT_EQ(0, close(dgram_pathname_socket));
>
> if (WIFSIGNALED(status) || !WIFEXITED(status) ||
> WEXITSTATUS(status) != EXIT_SUCCESS)
> _metadata->exit_code = KSFT_FAIL;
> }
>
> -TEST(datagram_sockets)
> +/* Fixture for datagram_sockets and self_connect tests */
> +FIXTURE(socket_type_test)
> {
> struct service_fixture connected_addr, non_connected_addr;
> +};
> +
> +FIXTURE_VARIANT(socket_type_test)
> +{
> + const bool abstract;
> +};
> +
> +/* clang-format off */
> +FIXTURE_VARIANT_ADD(socket_type_test, abstract) {
> + /* clang-format on */
> + .abstract = true,
> +};
> +
> +/* clang-format off */
> +FIXTURE_VARIANT_ADD(socket_type_test, pathname) {
> + /* clang-format on */
> + .abstract = false,
> +};
> +
> +FIXTURE_SETUP(socket_type_test)
> +{
> + drop_caps(_metadata);
> +
> + if (!variant->abstract)
> + ASSERT_EQ(0, mkdir(PATHNAME_UNIX_SOCK_DIR, 0700));
> +
> + memset(&self->connected_addr, 0, sizeof(self->connected_addr));
> + set_unix_address(&self->connected_addr, 0, variant->abstract);
> + memset(&self->non_connected_addr, 0, sizeof(self->non_connected_addr));
> + set_unix_address(&self->non_connected_addr, 1, variant->abstract);
> +}
> +
> +FIXTURE_TEARDOWN(socket_type_test)
> +{
> + if (!variant->abstract) {
> + EXPECT_EQ(0, remove_path(self->connected_addr.unix_addr.sun_path));
> + EXPECT_EQ(0, remove_path(self->non_connected_addr.unix_addr.sun_path));
> + EXPECT_EQ(0, rmdir(PATHNAME_UNIX_SOCK_DIR));
> + }
> +}
> +
> +TEST_F(socket_type_test, datagram_sockets)
> +{
> int server_conn_socket, server_unconn_socket;
> int pipe_parent[2], pipe_child[2];
> int status;
> char buf;
> pid_t child;
> -
> - drop_caps(_metadata);
> - memset(&connected_addr, 0, sizeof(connected_addr));
> - set_unix_address(&connected_addr, 0, true);
> - memset(&non_connected_addr, 0, sizeof(non_connected_addr));
> - set_unix_address(&non_connected_addr, 1, true);
> + const __u16 scope = variant->abstract ?
> + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET :
> + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
>
> ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
> ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
> @@ -1169,8 +1362,9 @@ TEST(datagram_sockets)
> /* Waits for parent to listen. */
> ASSERT_EQ(1, read(pipe_parent[0], &buf, 1));
> ASSERT_EQ(0,
> - connect(client_conn_socket, &connected_addr.unix_addr,
> - connected_addr.unix_addr_len));
> + connect(client_conn_socket,
> + &self->connected_addr.unix_addr,
> + self->connected_addr.unix_addr_len));
>
> /*
> * Both connected and non-connected sockets can send data when
> @@ -1178,13 +1372,12 @@ TEST(datagram_sockets)
> */
> ASSERT_EQ(1, send(client_conn_socket, ".", 1, 0));
> ASSERT_EQ(1, sendto(client_unconn_socket, ".", 1, 0,
> - &non_connected_addr.unix_addr,
> - non_connected_addr.unix_addr_len));
> + &self->non_connected_addr.unix_addr,
> + self->non_connected_addr.unix_addr_len));
> ASSERT_EQ(1, write(pipe_child[1], ".", 1));
>
> /* Scopes the domain. */
> - create_scoped_domain(_metadata,
> - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
>
> /*
> * Connected socket sends data to the receiver, but the
> @@ -1192,8 +1385,8 @@ TEST(datagram_sockets)
> */
> ASSERT_EQ(1, send(client_conn_socket, ".", 1, 0));
> ASSERT_EQ(-1, sendto(client_unconn_socket, ".", 1, 0,
> - &non_connected_addr.unix_addr,
> - non_connected_addr.unix_addr_len));
> + &self->non_connected_addr.unix_addr,
> + self->non_connected_addr.unix_addr_len));
> ASSERT_EQ(EPERM, errno);
> ASSERT_EQ(1, write(pipe_child[1], ".", 1));
>
> @@ -1210,10 +1403,11 @@ TEST(datagram_sockets)
> ASSERT_LE(0, server_conn_socket);
> ASSERT_LE(0, server_unconn_socket);
>
> - ASSERT_EQ(0, bind(server_conn_socket, &connected_addr.unix_addr,
> - connected_addr.unix_addr_len));
> - ASSERT_EQ(0, bind(server_unconn_socket, &non_connected_addr.unix_addr,
> - non_connected_addr.unix_addr_len));
> + ASSERT_EQ(0, bind(server_conn_socket, &self->connected_addr.unix_addr,
> + self->connected_addr.unix_addr_len));
> + ASSERT_EQ(0, bind(server_unconn_socket,
> + &self->non_connected_addr.unix_addr,
> + self->non_connected_addr.unix_addr_len));
> ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
>
> /* Waits for child to test. */
> @@ -1238,52 +1432,49 @@ TEST(datagram_sockets)
> _metadata->exit_code = KSFT_FAIL;
> }
>
> -TEST(self_connect)
> +TEST_F(socket_type_test, self_connect)
> {
> - struct service_fixture connected_addr, non_connected_addr;
> int connected_socket, non_connected_socket, status;
> pid_t child;
> -
> - drop_caps(_metadata);
> - memset(&connected_addr, 0, sizeof(connected_addr));
> - set_unix_address(&connected_addr, 0, true);
> - memset(&non_connected_addr, 0, sizeof(non_connected_addr));
> - set_unix_address(&non_connected_addr, 1, true);
> + const __u16 scope = variant->abstract ?
> + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET :
> + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
>
> connected_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
> non_connected_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
> ASSERT_LE(0, connected_socket);
> ASSERT_LE(0, non_connected_socket);
>
> - ASSERT_EQ(0, bind(connected_socket, &connected_addr.unix_addr,
> - connected_addr.unix_addr_len));
> - ASSERT_EQ(0, bind(non_connected_socket, &non_connected_addr.unix_addr,
> - non_connected_addr.unix_addr_len));
> + ASSERT_EQ(0, bind(connected_socket, &self->connected_addr.unix_addr,
> + self->connected_addr.unix_addr_len));
> + ASSERT_EQ(0, bind(non_connected_socket,
> + &self->non_connected_addr.unix_addr,
> + self->non_connected_addr.unix_addr_len));
>
> child = fork();
> ASSERT_LE(0, child);
> if (child == 0) {
> /* Child's domain is scoped. */
> - create_scoped_domain(_metadata,
> - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + create_scoped_domain(_metadata, scope);
>
> /*
> * The child inherits the sockets, and cannot connect or
> * send data to them.
> */
> ASSERT_EQ(-1,
> - connect(connected_socket, &connected_addr.unix_addr,
> - connected_addr.unix_addr_len));
> + connect(connected_socket,
> + &self->connected_addr.unix_addr,
> + self->connected_addr.unix_addr_len));
> ASSERT_EQ(EPERM, errno);
>
> ASSERT_EQ(-1, sendto(connected_socket, ".", 1, 0,
> - &connected_addr.unix_addr,
> - connected_addr.unix_addr_len));
> + &self->connected_addr.unix_addr,
> + self->connected_addr.unix_addr_len));
> ASSERT_EQ(EPERM, errno);
>
> ASSERT_EQ(-1, sendto(non_connected_socket, ".", 1, 0,
> - &non_connected_addr.unix_addr,
> - non_connected_addr.unix_addr_len));
> + &self->non_connected_addr.unix_addr,
> + self->non_connected_addr.unix_addr_len));
> ASSERT_EQ(EPERM, errno);
>
> EXPECT_EQ(0, close(connected_socket));
> --
> 2.52.0
>
>
^ permalink raw reply
* Re: [PATCH v2 2/6] landlock: Implement LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET
From: Mickaël Salaün @ 2026-01-29 21:27 UTC (permalink / raw)
To: Tingmao Wang
Cc: Günther Noack, Demi Marie Obenour, Alyssa Ross, Jann Horn,
Tahera Fahimi, Justin Suess, linux-security-module
In-Reply-To: <a6d6479888d9d216a3f2e7bb133523f856f92461.1767115163.git.m@maowtm.org>
On Tue, Dec 30, 2025 at 05:20:20PM +0000, Tingmao Wang wrote:
> Extend the existing abstract UNIX socket scoping to pathname sockets as
> well. Basically all of the logic is reused between the two types, just
> that pathname sockets scoping are controlled by another bit, and has its
> own audit request type (since the current one is named
> "abstract_unix_socket").
>
> Closes: https://github.com/landlock-lsm/linux/issues/51
> Signed-off-by: Tingmao Wang <m@maowtm.org>
> ---
>
> Changes in v2:
> - Factor out common code in hook_unix_stream_connect and
> hook_unix_may_send into check_socket_access(), and inline
> is_abstract_socket().
>
> security/landlock/audit.c | 4 ++
> security/landlock/audit.h | 1 +
> security/landlock/task.c | 109 ++++++++++++++++++++++----------------
> 3 files changed, 67 insertions(+), 47 deletions(-)
>
> diff --git a/security/landlock/audit.c b/security/landlock/audit.c
> index e899995f1fd5..0626cc553ab0 100644
> --- a/security/landlock/audit.c
> +++ b/security/landlock/audit.c
> @@ -75,6 +75,10 @@ get_blocker(const enum landlock_request_type type,
> WARN_ON_ONCE(access_bit != -1);
> return "scope.abstract_unix_socket";
>
> + case LANDLOCK_REQUEST_SCOPE_PATHNAME_UNIX_SOCKET:
> + WARN_ON_ONCE(access_bit != -1);
> + return "scope.pathname_unix_socket";
> +
> case LANDLOCK_REQUEST_SCOPE_SIGNAL:
> WARN_ON_ONCE(access_bit != -1);
> return "scope.signal";
> diff --git a/security/landlock/audit.h b/security/landlock/audit.h
> index 92428b7fc4d8..1c9ce8588102 100644
> --- a/security/landlock/audit.h
> +++ b/security/landlock/audit.h
> @@ -21,6 +21,7 @@ enum landlock_request_type {
> LANDLOCK_REQUEST_NET_ACCESS,
> LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
> LANDLOCK_REQUEST_SCOPE_SIGNAL,
> + LANDLOCK_REQUEST_SCOPE_PATHNAME_UNIX_SOCKET,
> };
>
> /*
> diff --git a/security/landlock/task.c b/security/landlock/task.c
> index 833bc0cfe5c9..10dc356baf6f 100644
> --- a/security/landlock/task.c
> +++ b/security/landlock/task.c
> @@ -232,35 +232,81 @@ static bool domain_is_scoped(const struct landlock_ruleset *const client,
> return false;
> }
>
> +/**
> + * sock_is_scoped - Check if socket connect or send should be restricted
> + * based on scope controls.
> + *
> + * @other: The server socket.
> + * @domain: The client domain.
> + * @scope: The relevant scope bit to check (i.e. pathname or abstract).
> + *
> + * Returns: True if connect should be restricted, false otherwise.
> + */
> static bool sock_is_scoped(struct sock *const other,
> - const struct landlock_ruleset *const domain)
> + const struct landlock_ruleset *const domain,
> + access_mask_t scope)
> {
> const struct landlock_ruleset *dom_other;
>
> /* The credentials will not change. */
> lockdep_assert_held(&unix_sk(other)->lock);
> dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain;
> - return domain_is_scoped(domain, dom_other,
> - LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
> + return domain_is_scoped(domain, dom_other, scope);
> }
>
> -static bool is_abstract_socket(struct sock *const sock)
> +/* Allow us to quickly test if the current domain scopes any form of socket */
Missing final dot.
> +static const struct access_masks unix_scope = {
> + .scope = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
> + LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET,
> +};
> +
> +/*
> + * UNIX sockets can have three types of addresses: pathname (a filesystem path),
> + * unnamed (not bound to an address), and abstract (sun_path[0] is '\0').
> + * Unnamed sockets include those created with socketpair() and unbound sockets.
> + * We do not restrict unnamed sockets since they have no address to identify.
Not because they have no address but because they cannot be used to
reach a new peer, right?
> + */
> +static int
> +check_socket_access(struct sock *const other,
> + const struct landlock_cred_security *const subject,
> + const size_t handle_layer)
> {
> - struct unix_address *addr = unix_sk(sock)->addr;
> + const struct unix_address *addr = unix_sk(other)->addr;
> + access_mask_t scope;
> + enum landlock_request_type request_type;
>
> + /* Unnamed sockets are not restricted. */
> if (!addr)
> - return false;
> + return 0;
>
> + /*
> + * Abstract and pathname Unix sockets have separate scope and audit
UNIX
> + * request type.
> + */
> if (addr->len >= offsetof(struct sockaddr_un, sun_path) + 1 &&
> - addr->name->sun_path[0] == '\0')
> - return true;
> + addr->name->sun_path[0] == '\0') {
> + scope = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET;
> + request_type = LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET;
> + } else {
> + scope = LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET;
> + request_type = LANDLOCK_REQUEST_SCOPE_PATHNAME_UNIX_SOCKET;
> + }
>
> - return false;
> -}
> + if (!sock_is_scoped(other, subject->domain, scope))
> + return 0;
>
> -static const struct access_masks unix_scope = {
> - .scope = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
> -};
> + landlock_log_denial(subject, &(struct landlock_request) {
> + .type = request_type,
> + .audit = {
> + .type = LSM_AUDIT_DATA_NET,
> + .u.net = &(struct lsm_network_audit) {
> + .sk = other,
> + },
> + },
> + .layer_plus_one = handle_layer + 1,
> + });
> + return -EPERM;
> +}
>
> static int hook_unix_stream_connect(struct sock *const sock,
> struct sock *const other,
> @@ -275,23 +321,7 @@ static int hook_unix_stream_connect(struct sock *const sock,
> if (!subject)
> return 0;
>
> - if (!is_abstract_socket(other))
> - return 0;
> -
> - if (!sock_is_scoped(other, subject->domain))
> - return 0;
> -
> - landlock_log_denial(subject, &(struct landlock_request) {
> - .type = LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
> - .audit = {
> - .type = LSM_AUDIT_DATA_NET,
> - .u.net = &(struct lsm_network_audit) {
> - .sk = other,
> - },
> - },
> - .layer_plus_one = handle_layer + 1,
> - });
> - return -EPERM;
> + return check_socket_access(other, subject, handle_layer);
> }
>
> static int hook_unix_may_send(struct socket *const sock,
> @@ -302,6 +332,7 @@ static int hook_unix_may_send(struct socket *const sock,
> landlock_get_applicable_subject(current_cred(), unix_scope,
> &handle_layer);
>
> + /* Quick return for non-landlocked tasks. */
> if (!subject)
> return 0;
>
> @@ -312,23 +343,7 @@ static int hook_unix_may_send(struct socket *const sock,
> if (unix_peer(sock->sk) == other->sk)
> return 0;
>
> - if (!is_abstract_socket(other->sk))
> - return 0;
> -
> - if (!sock_is_scoped(other->sk, subject->domain))
> - return 0;
> -
> - landlock_log_denial(subject, &(struct landlock_request) {
> - .type = LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
> - .audit = {
> - .type = LSM_AUDIT_DATA_NET,
> - .u.net = &(struct lsm_network_audit) {
> - .sk = other->sk,
> - },
> - },
> - .layer_plus_one = handle_layer + 1,
> - });
> - return -EPERM;
> + return check_socket_access(other->sk, subject, handle_layer);
> }
>
> static const struct access_masks signal_scope = {
> --
> 2.52.0
>
^ permalink raw reply
* [PATCH] lsm: preserve /proc/sys/vm/mmap_min_addr when !CONFIG_SECURITY
From: Paul Moore @ 2026-01-29 22:51 UTC (permalink / raw)
To: linux-security-module; +Cc: linux-mm, lorenzo.stoakes
While reworking the LSM initialization code the
/proc/sys/vm/mmap_min_addr handler was inadvertently caught up in the
change and the procfs entry wasn't setup when CONFIG_SECURITY was not
selected at kernel build time. This patch restores the previous behavior
and ensures that the procfs entry is setup regardless of the
CONFIG_SECURITY state.
Future work will improve upon this, likely by moving the procfs handler
into the mm subsystem, but this patch should resolve the immediate
regression.
Fixes: 4ab5efcc2829 ("lsm: consolidate all of the LSM framework initcalls")
Reported-by: Lorenzo Stoakes <lorenzo.stoakes@oracle.com>
Signed-off-by: Paul Moore <paul@paul-moore.com>
---
security/lsm.h | 9 ---------
security/lsm_init.c | 7 +------
security/min_addr.c | 5 ++---
3 files changed, 3 insertions(+), 18 deletions(-)
diff --git a/security/lsm.h b/security/lsm.h
index 81aadbc61685..db77cc83e158 100644
--- a/security/lsm.h
+++ b/security/lsm.h
@@ -37,15 +37,6 @@ int lsm_task_alloc(struct task_struct *task);
/* LSM framework initializers */
-#ifdef CONFIG_MMU
-int min_addr_init(void);
-#else
-static inline int min_addr_init(void)
-{
- return 0;
-}
-#endif /* CONFIG_MMU */
-
#ifdef CONFIG_SECURITYFS
int securityfs_init(void);
#else
diff --git a/security/lsm_init.c b/security/lsm_init.c
index 05bd52e6b1f2..573e2a7250c4 100644
--- a/security/lsm_init.c
+++ b/security/lsm_init.c
@@ -489,12 +489,7 @@ int __init security_init(void)
*/
static int __init security_initcall_pure(void)
{
- int rc_adr, rc_lsm;
-
- rc_adr = min_addr_init();
- rc_lsm = lsm_initcall(pure);
-
- return (rc_adr ? rc_adr : rc_lsm);
+ return lsm_initcall(pure);
}
pure_initcall(security_initcall_pure);
diff --git a/security/min_addr.c b/security/min_addr.c
index 0fde5ec9abc8..56e4f9d25929 100644
--- a/security/min_addr.c
+++ b/security/min_addr.c
@@ -5,8 +5,6 @@
#include <linux/sysctl.h>
#include <linux/minmax.h>
-#include "lsm.h"
-
/* amount of vm to protect from userspace access by both DAC and the LSM*/
unsigned long mmap_min_addr;
/* amount of vm to protect from userspace using CAP_SYS_RAWIO (DAC) */
@@ -54,10 +52,11 @@ static const struct ctl_table min_addr_sysctl_table[] = {
},
};
-int __init min_addr_init(void)
+static int __init mmap_min_addr_init(void)
{
register_sysctl_init("vm", min_addr_sysctl_table);
update_mmap_min_addr();
return 0;
}
+pure_initcall(mmap_min_addr_init);
--
2.52.0
^ permalink raw reply related
* Re: [PATCH v5 10/11] lsm: consolidate all of the LSM framework initcalls
From: Paul Moore @ 2026-01-29 23:06 UTC (permalink / raw)
To: Lorenzo Stoakes
Cc: Vlastimil Babka, linux-security-module, linux-integrity, selinux,
john.johansen, zohar, roberto.sassu, wufan, mic, gnoack, kees,
mortonm, casey, penguin-kernel, nicolas.bouchinet, xiujianfeng,
linux-mm, David Hildenbrand, Liam R. Howlett, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel
In-Reply-To: <5392220f-c29e-4cbd-8dae-59fbea4c6491@lucifer.local>
On Thu, Jan 29, 2026 at 1:59 PM Lorenzo Stoakes
<lorenzo.stoakes@oracle.com> wrote:
> On Thu, Jan 29, 2026 at 01:31:05PM -0500, Paul Moore wrote:
> > On Thu, Jan 29, 2026 at 12:11 PM Lorenzo Stoakes
> > <lorenzo.stoakes@oracle.com> wrote:
> > > On Thu, Jan 29, 2026 at 06:02:00PM +0100, Vlastimil Babka wrote:
...
> > > But that's future work, for an -rc8 hotfix ...
> >
> > Expect a patch later today.
>
> Perfect thank you very much!
A link to the patch is below. Assuming I don't hear any negative
comments, I'll plan to merge this into lsm/stable-6.19 tomorrow and
send it up to Linus early next week; this should give us a day or two
in linux-next and then most of the week in Linus' tree before the
v6.19 release.
https://lore.kernel.org/linux-security-module/20260129225132.420484-2-paul@paul-moore.com
--
paul-moore.com
^ permalink raw reply
* [PATCH] ipe: document AT_EXECVE_CHECK TOCTOU issue on OverlayFS
From: wufan @ 2026-01-30 0:14 UTC (permalink / raw)
To: linux-security-module
Cc: linux-doc, linux-kernel, corbet, mic, miklos, amir73il,
linux-unionfs, Fan Wu
From: Fan Wu <wufan@kernel.org>
Document a known TOCTOU (time-of-check to time-of-use) issue when using
AT_EXECVE_CHECK with read() on OverlayFS. The deny_write_access()
protection is only held during the syscall, allowing a copy-up operation
to be triggered afterward, causing subsequent read() calls to return
content from the unprotected upper layer.
This is generally not a concern for typical IPE deployments since
dm-verity and fs-verity protected files are effectively read-only.
However, OverlayFS with a writable upper layer presents a special case.
Document mitigation strategies including mounting overlay as read-only
and using mmap() instead of read(). Note that the mmap() mitigation
relies on current OverlayFS implementation details and should not be
considered a security guarantee.
Signed-off-by: Fan Wu <wufan@kernel.org>
---
Documentation/admin-guide/LSM/ipe.rst | 32 +++++++++++++++++++++++++++
1 file changed, 32 insertions(+)
diff --git a/Documentation/admin-guide/LSM/ipe.rst b/Documentation/admin-guide/LSM/ipe.rst
index a756d8158531..b621a98fe5e2 100644
--- a/Documentation/admin-guide/LSM/ipe.rst
+++ b/Documentation/admin-guide/LSM/ipe.rst
@@ -110,6 +110,34 @@ intercepts during the execution process, this mechanism needs the interpreter
to take the initiative, and existing interpreters won't be automatically
supported unless the signal call is added.
+.. WARNING::
+
+ There is a known TOCTOU (time-of-check to time-of-use) issue with
+ ``AT_EXECVE_CHECK`` when interpreters use ``read()`` to obtain script
+ contents after the check [#atacexecvecheck_toctou]_. The ``AT_EXECVE_CHECK``
+ protection (via ``deny_write_access()``) is only held during the syscall.
+ After it returns, the file can be modified before the interpreter reads it.
+
+ In typical IPE deployments, this is not a concern because files protected
+ by dm-verity or fs-verity are effectively read-only and cannot be modified.
+ However, OverlayFS presents a special case: when the lower layer is
+ dm-verity protected (read-only) but the upper layer is writable, an
+ attacker with write access can trigger a copy-up operation after the
+ ``AT_EXECVE_CHECK`` returns, causing subsequent ``read()`` calls to return
+ content from the unprotected upper layer instead of the verified lower layer.
+
+ To mitigate this issue on OverlayFS:
+
+ - Mount the overlay as read-only, or restrict write access to the upper
+ layer.
+ - Interpreters may use ``mmap()`` instead of ``read()`` to obtain script
+ contents. Currently, OverlayFS fixes the underlying real file reference
+ at ``open()`` time for mmap operations, so mmap will continue to access
+ the original lower layer file even after a copy-up. However, this
+ behavior is an implementation detail of OverlayFS and is not guaranteed
+ to remain stable across kernel versions. Do not rely on this as a
+ security guarantee.
+
Threat Model
------------
@@ -833,3 +861,7 @@ A:
kernel's fsverity support; IPE does not impose any
restrictions on the digest algorithm itself;
thus, this list may be out of date.
+
+.. [#atacexecvecheck_toctou] See the O_DENY_WRITE RFC discussion for details on
+ this TOCTOU issue:
+ https://lore.kernel.org/all/20250822170800.2116980-1-mic@digikod.net/
--
2.52.0
^ permalink raw reply related
* Re: [PATCH] lsm: preserve /proc/sys/vm/mmap_min_addr when !CONFIG_SECURITY
From: Kees Cook @ 2026-01-30 1:31 UTC (permalink / raw)
To: Paul Moore; +Cc: linux-security-module, linux-mm, lorenzo.stoakes
In-Reply-To: <20260129225132.420484-2-paul@paul-moore.com>
On Thu, Jan 29, 2026 at 05:51:33PM -0500, Paul Moore wrote:
> While reworking the LSM initialization code the
> /proc/sys/vm/mmap_min_addr handler was inadvertently caught up in the
> change and the procfs entry wasn't setup when CONFIG_SECURITY was not
> selected at kernel build time. This patch restores the previous behavior
> and ensures that the procfs entry is setup regardless of the
> CONFIG_SECURITY state.
>
> Future work will improve upon this, likely by moving the procfs handler
> into the mm subsystem, but this patch should resolve the immediate
> regression.
>
> Fixes: 4ab5efcc2829 ("lsm: consolidate all of the LSM framework initcalls")
> Reported-by: Lorenzo Stoakes <lorenzo.stoakes@oracle.com>
> Signed-off-by: Paul Moore <paul@paul-moore.com>
Good catch and fix!
Reviewed-by: Kees Cook <kees@kernel.org>
-Kees
> ---
> security/lsm.h | 9 ---------
> security/lsm_init.c | 7 +------
> security/min_addr.c | 5 ++---
> 3 files changed, 3 insertions(+), 18 deletions(-)
>
> diff --git a/security/lsm.h b/security/lsm.h
> index 81aadbc61685..db77cc83e158 100644
> --- a/security/lsm.h
> +++ b/security/lsm.h
> @@ -37,15 +37,6 @@ int lsm_task_alloc(struct task_struct *task);
>
> /* LSM framework initializers */
>
> -#ifdef CONFIG_MMU
> -int min_addr_init(void);
> -#else
> -static inline int min_addr_init(void)
> -{
> - return 0;
> -}
> -#endif /* CONFIG_MMU */
> -
> #ifdef CONFIG_SECURITYFS
> int securityfs_init(void);
> #else
> diff --git a/security/lsm_init.c b/security/lsm_init.c
> index 05bd52e6b1f2..573e2a7250c4 100644
> --- a/security/lsm_init.c
> +++ b/security/lsm_init.c
> @@ -489,12 +489,7 @@ int __init security_init(void)
> */
> static int __init security_initcall_pure(void)
> {
> - int rc_adr, rc_lsm;
> -
> - rc_adr = min_addr_init();
> - rc_lsm = lsm_initcall(pure);
> -
> - return (rc_adr ? rc_adr : rc_lsm);
> + return lsm_initcall(pure);
> }
> pure_initcall(security_initcall_pure);
>
> diff --git a/security/min_addr.c b/security/min_addr.c
> index 0fde5ec9abc8..56e4f9d25929 100644
> --- a/security/min_addr.c
> +++ b/security/min_addr.c
> @@ -5,8 +5,6 @@
> #include <linux/sysctl.h>
> #include <linux/minmax.h>
>
> -#include "lsm.h"
> -
> /* amount of vm to protect from userspace access by both DAC and the LSM*/
> unsigned long mmap_min_addr;
> /* amount of vm to protect from userspace using CAP_SYS_RAWIO (DAC) */
> @@ -54,10 +52,11 @@ static const struct ctl_table min_addr_sysctl_table[] = {
> },
> };
>
> -int __init min_addr_init(void)
> +static int __init mmap_min_addr_init(void)
> {
> register_sysctl_init("vm", min_addr_sysctl_table);
> update_mmap_min_addr();
>
> return 0;
> }
> +pure_initcall(mmap_min_addr_init);
> --
> 2.52.0
>
--
Kees Cook
^ permalink raw reply
* Re: [PATCH v4] ima: Fallback to ctime check for FS without kstat.change_cookie
From: Mimi Zohar @ 2026-01-30 2:32 UTC (permalink / raw)
To: Frederick Lawler, Roberto Sassu, Dmitry Kasatkin, Eric Snowberg,
Paul Moore, James Morris, Serge E. Hallyn, Darrick J. Wong,
Christian Brauner, Josef Bacik, Jeff Layton
Cc: linux-kernel, linux-integrity, linux-security-module, kernel-team
In-Reply-To: <20260129-xfs-ima-fixup-v4-1-6bb89df7b6a3@cloudflare.com>
On Thu, 2026-01-29 at 12:07 -0600, Frederick Lawler wrote:
> Commit 1cf7e834a6fb ("xfs: switch to multigrain timestamps")
> introduced a means to track change detection for an inode
> via ctime updates, opposed to setting kstat.change_cookie to
> an i_version when calling into xfs_vn_getattr().
>
> This introduced a regression for IMA such that an action
> performed on a LOWER inode on a stacked file systems always
> requires a re-evaluation if the LOWER file system does not
> leverage kstat.change_cookie to track inode i_version or lacks
> i_version support all together.
Please describe the change in behavior that needs to be fixed. Are there too
many, too few measurements, or both?
Examples are fine, but first describe the problem - not detecting file change on
xfs.
>
> In the case of stacking XFS on XFS, an action on either the LOWER or UPPER
> will require re-evaluation. Stacking TMPFS on XFS for instance, once the
> inode is UPPER is mutated, IMA resumes normal behavior because TMPFS
> leverages generic_fillattr() to update the change cookie.
This sounds like the same issue - not detecting file change on xfs. The problem
is simply manifesting itself on stacked filesystems.
>
> This is because IMA caches kstat.change_cookie to compare against an
> inode's i_version directly in integrity_inode_attrs_changed(), and thus
> could be out of date depending on how file systems set
> kstat.change_cookie.
>
> To address this, require integrity_inode_attrs_changed() to query
> vfs_getattr_nosec() to compare the cached version against
> kstat.change_cookie directly. This ensures that when updates occur,
> we're accessing the same changed inode version on changes, and fallback
> to compare against kstat.ctime when STATX_CHANGE_COOKIE is missing from
> result mask.
>
> Lastly, because EVM still relies on querying and caching a inode's
> i_version directly, the integrity_inode_attrs_changed() falls back to the
> original inode.i_version != cached comparison.
>
> Link: https://lore.kernel.org/all/aTspr4_h9IU4EyrR@CMGLRV3
> Fixes: 1cf7e834a6fb ("xfs: switch to multigrain timestamps")
> Suggested-by: Jeff Layton <jlayton@kernel.org>
> Reviewed-by: Jeff Layton <jlayton@kernel.org>
> Signed-off-by: Frederick Lawler <fred@cloudflare.com>
> ---
> include/linux/integrity.h | 35 +++++++++++++++++++++++++++++++----
> security/integrity/evm/evm_main.c | 5 ++---
> security/integrity/ima/ima_api.c | 11 ++++++++---
> security/integrity/ima/ima_main.c | 17 ++++++++++-------
> 4 files changed, 51 insertions(+), 17 deletions(-)
>
> diff --git a/include/linux/integrity.h b/include/linux/integrity.h
> index f5842372359be5341b6870a43b92e695e8fc78af..034f0a1ed48ca8c19c764e302bbfc555dad92cde 100644
> --- a/include/linux/integrity.h
> +++ b/include/linux/integrity.h
> @@ -9,6 +9,8 @@
>
> #include <linux/fs.h>
> #include <linux/iversion.h>
> +#include <linux/kernel.h>
> +#include <linux/time64.h>
>
> enum integrity_status {
> INTEGRITY_PASS = 0,
> @@ -51,14 +53,39 @@ integrity_inode_attrs_store(struct integrity_inode_attributes *attrs,
>
> /*
> * On stacked filesystems detect whether the inode or its content has changed.
> + *
> + * Must be called in process context.
> */
> static inline bool
> integrity_inode_attrs_changed(const struct integrity_inode_attributes *attrs,
> - const struct inode *inode)
> + struct file *file, struct inode *inode)
> {
> - return (inode->i_sb->s_dev != attrs->dev ||
> - inode->i_ino != attrs->ino ||
> - !inode_eq_iversion(inode, attrs->version));
> + struct kstat stat;
> +
> + might_sleep();
> +
> + if (inode->i_sb->s_dev != attrs->dev || inode->i_ino != attrs->ino)
> + return true;
> +
> + /*
> + * EVM currently relies on backing inode i_version. While IS_I_VERSION
> + * is not a good indicator of i_version support, this still retains
> + * the logic such that a re-evaluation should still occur for EVM, and
> + * only for IMA if vfs_getattr_nosec() fails.
> + */
> + if (!file || vfs_getattr_nosec(&file->f_path, &stat,
> + STATX_CHANGE_COOKIE | STATX_CTIME,
> + AT_STATX_SYNC_AS_STAT))
> + return !IS_I_VERSION(inode) ||
> + !inode_eq_iversion(inode, attrs->version);
> +
> + if (stat.result_mask & STATX_CHANGE_COOKIE)
> + return stat.change_cookie != attrs->version;
> +
> + if (stat.result_mask & STATX_CTIME)
> + return timespec64_to_ns(&stat.ctime) != (s64)attrs->version;
> +
> + return true;
> }
>
>
> diff --git a/security/integrity/ima/ima_main.c b/security/integrity/ima/ima_main.c
> index 5770cf691912aa912fc65280c59f5baac35dd725..8ac42b03740eb93bf23b15cb9039af6cd32aa999 100644
> --- a/security/integrity/ima/ima_main.c
> +++ b/security/integrity/ima/ima_main.c
> @@ -28,6 +28,7 @@
> #include <linux/iversion.h>
> #include <linux/evm.h>
> #include <linux/crash_dump.h>
> +#include <linux/time64.h>
>
> #include "ima.h"
>
> @@ -199,10 +200,13 @@ static void ima_check_last_writer(struct ima_iint_cache *iint,
> &iint->atomic_flags);
> if ((iint->flags & IMA_NEW_FILE) ||
> vfs_getattr_nosec(&file->f_path, &stat,
> - STATX_CHANGE_COOKIE,
> - AT_STATX_SYNC_AS_STAT) ||
> - !(stat.result_mask & STATX_CHANGE_COOKIE) ||
> - stat.change_cookie != iint->real_inode.version) {
> + STATX_CHANGE_COOKIE | STATX_CTIME,
> + AT_STATX_SYNC_AS_STAT) ||
> + ((stat.result_mask & STATX_CHANGE_COOKIE) ?
> + stat.change_cookie != iint->real_inode.version :
> + (!(stat.result_mask & STATX_CTIME) ||
> + timespec64_to_ns(&stat.ctime) !=
> + (s64)iint->real_inode.version))) {
> iint->flags &= ~(IMA_DONE_MASK | IMA_NEW_FILE);
> iint->measured_pcrs = 0;
> if (update)
The original i_version test was clear. This code has become really hard to
understand and needs to be cleaned up. Defining a helper function, will also
avoid code duplication.
Mimi
^ permalink raw reply
* [syzbot] Monthly lsm report (Jan 2026)
From: syzbot @ 2026-01-30 10:09 UTC (permalink / raw)
To: linux-kernel, linux-security-module, syzkaller-bugs
Hello lsm maintainers/developers,
This is a 31-day syzbot report for the lsm subsystem.
All related reports/information can be found at:
https://syzkaller.appspot.com/upstream/s/lsm
During the period, 1 new issues were detected and 0 were fixed.
In total, 5 issues are still open and 42 have already been fixed.
Some of the still happening issues:
Ref Crashes Repro Title
<1> 65 Yes INFO: task hung in process_measurement (3)
https://syzkaller.appspot.com/bug?extid=cb9e66807bcb882cd0c5
<2> 44 Yes possible deadlock in keyring_clear (3)
https://syzkaller.appspot.com/bug?extid=f55b043dacf43776b50c
<3> 1 Yes memory leak in prepare_creds (6)
https://syzkaller.appspot.com/bug?extid=dd3b43aa0204089217ee
---
This report is generated by a bot. It may contain errors.
See https://goo.gl/tpsmEJ for more information about syzbot.
syzbot engineers can be reached at syzkaller@googlegroups.com.
To disable reminders for individual bugs, reply with the following command:
#syz set <Ref> no-reminders
To change bug's subsystems, reply with:
#syz set <Ref> subsystems: new-subsystem
You may send multiple commands in a single email message.
^ permalink raw reply
* Re: [PATCH net-next v2 1/4] selftests: net: Move some UAPI header inclusions after libc ones
From: Thomas Weißschuh @ 2026-01-30 10:22 UTC (permalink / raw)
To: Matthieu Baerts
Cc: netdev, linux-kernel, linux-api, Arnd Bergmann, linux-kselftest,
mptcp, linux-security-module, bpf, libc-alpha
In-Reply-To: <4e673f7f-c49c-46b0-85b4-bae6e4efcb3a@kernel.org>
(trimmed the recipient list)
Hi Matt,
On Mon, Jan 26, 2026 at 07:13:50PM +0100, Matthieu Baerts wrote:
> On 20/01/2026 15:10, Thomas Weißschuh wrote:
> > Interleaving inclusions of UAPI headers and libc headers is problematic.
> > Both sets of headers define conflicting symbols. To enable their
> > coexistence a compatibility-mechanism is in place.
> >
> > An upcoming change will define 'struct sockaddr' from linux/socket.h.
> > However sys/socket.h from libc does not yet handle this case and a
> > symbol conflict will arise.
> >
> > Furthermore libc-compat.h evaluates the state of the libc
> > inclusions only once, at the point it is included first. If another
> > problematic header from libc is included later, symbol conflicts arise.
> > This will trigger other duplicate definitions when linux/libc-compat.h
> > is added to linux/socket.h
> >
> > Move the inclusion of UAPI headers after the inclusion of the glibc
> > ones, so the libc-compat.h continues to work correctly.
>
> Thank you for looking at this!
>
> Here is my (late, sorry) review for the modifications related to MPTCP:
> > diff --git a/tools/testing/selftests/net/mptcp/mptcp_diag.c b/tools/testing/selftests/net/mptcp/mptcp_diag.c
> > index 8e0b1b8d84b6..af25ebfd2915 100644
> > --- a/tools/testing/selftests/net/mptcp/mptcp_diag.c
> > +++ b/tools/testing/selftests/net/mptcp/mptcp_diag.c
> > @@ -1,11 +1,6 @@
> > // SPDX-License-Identifier: GPL-2.0
> > /* Copyright (c) 2025, Kylin Software */
> >
> > -#include <linux/sock_diag.h>
> > -#include <linux/rtnetlink.h>
> > -#include <linux/inet_diag.h>
> > -#include <linux/netlink.h>
> > -#include <linux/compiler.h>
> > #include <sys/socket.h>
> > #include <netinet/in.h>
> > #include <linux/tcp.h>
>
> There is a remaining one (linux/tcp.h) here that you might want to move
> below too.
Good point.
> > @@ -17,6 +12,12 @@
> > #include <errno.h>
> > #include <stdio.h>
> >
> > +#include <linux/sock_diag.h>
> > +#include <linux/rtnetlink.h>
> > +#include <linux/inet_diag.h>
> > +#include <linux/netlink.h>
> > +#include <linux/compiler.h>
>
> Note that I just noticed this is the only file from this directory where
> the "includes" are not sorted by type and alphabetical order, see
> pm_nl_ctl.c as an example. A bit of a detail, but if you plan to send a
> v2, do you mind doing that too here while at it, please?
I'll send a v3 during the next cycle.
> If not, I can look at that later, but better to avoid doing that in
> parallel.
If you want to fix this up already during this cycle,
that would also be most welcome.
Thomas
^ permalink raw reply
* Re: [PATCH net-next v2 0/4] net: uapi: Provide an UAPI definition of 'struct sockaddr'
From: Thomas Weißschuh @ 2026-01-30 10:34 UTC (permalink / raw)
To: Jakub Kicinski
Cc: Eric Dumazet, Kuniyuki Iwashima, Paolo Abeni, Willem de Bruijn,
David S. Miller, Simon Horman, Shuah Khan, Matthieu Baerts,
Mat Martineau, Geliang Tang, Mickaël Salaün,
Günther Noack, Alexei Starovoitov, Daniel Borkmann,
Jesper Dangaard Brouer, John Fastabend, Stanislav Fomichev,
Andrii Nakryiko, Martin KaFai Lau, Eduard Zingerman, Song Liu,
Yonghong Song, KP Singh, Hao Luo, Jiri Olsa, netdev, linux-kernel,
linux-api, Arnd Bergmann, linux-kselftest, mptcp,
linux-security-module, bpf, libc-alpha, Carlos O'Donell,
Adhemerval Zanella, Rich Felker, klibc, Florian Weimer
In-Reply-To: <20260121192729.2095aa25@kernel.org>
On Wed, Jan 21, 2026 at 07:27:29PM -0800, Jakub Kicinski wrote:
> On Tue, 20 Jan 2026 15:10:30 +0100 Thomas Weißschuh wrote:
> > Various UAPI headers reference 'struct sockaddr'. Currently the
> > definition of this struct is pulled in from the libc header
> > sys/socket.h. This is problematic as it introduces a dependency
> > on a full userspace toolchain.
> >
> > Add a definition of 'struct sockaddr' to the UAPI headers.
> > Before that, reorder some problematic header inclusions in the selftests.
>
> > include/linux/socket.h | 10 ----------
> > include/uapi/linux/if.h | 4 ----
> > include/uapi/linux/libc-compat.h | 12 ++++++++++++
> > include/uapi/linux/socket.h | 14 ++++++++++++++
> > samples/bpf/xdp_adjust_tail_user.c | 6 ++++--
> > samples/bpf/xdp_fwd_user.c | 7 ++++---
> > samples/bpf/xdp_router_ipv4_user.c | 6 +++---
> > samples/bpf/xdp_sample_user.c | 15 ++++++++-------
> > samples/bpf/xdp_tx_iptunnel_user.c | 4 ++--
> > tools/testing/selftests/landlock/audit.h | 7 ++++---
> > tools/testing/selftests/net/af_unix/diag_uid.c | 9 +++++----
> > tools/testing/selftests/net/busy_poller.c | 3 ++-
> > tools/testing/selftests/net/mptcp/mptcp_diag.c | 11 ++++++-----
> > tools/testing/selftests/net/nettest.c | 4 ++--
> > tools/testing/selftests/net/tcp_ao/icmps-discard.c | 6 +++---
> > tools/testing/selftests/net/tcp_ao/lib/netlink.c | 9 +++++----
> > tools/testing/selftests/net/tun.c | 5 +++--
> > 17 files changed, 77 insertions(+), 55 deletions(-)
>
> Are all those selftests / samples getting broken by this patch set?
Yes.
Some of them get broken by the new 'struct sockaddr', but some others are
already broken just by the new transitive inclusion of libc-compat.h.
So any header starting to use the compatibility machinery may trigger breakage
in code including UAPI headers before libc header, even for completely new type
definitions which themselves would not conflict with libc.
> I understand that we should avoid libc dependencies in uAPI but at
> least speaking for networking - building selftests without libc is..
> not a practical proposition?
I am not sure I understand. Some sort of libc will always be necessary.
And as the selftests are intended to exercise the low-level kernel APIs,
even those not supported by libc, the UAPI headers will also be necessary.
There is nolibc (tools/include/nolibc/) which is using the UAPI headers in
most cases, and aims to be compatible. And can be and already is used for
selftests, but it will be too limited for all of the networking selftests.
(Disclaimer: I am maintaining nolibc)
My goal is *not* to make the different headers less compatible on purpose.
But by removing the existing dependencies we can now enforce the checks in
CONFIG_UAPI_HEADER_TEST to prevent any new ones from creeping in. Therefore
preventing compatiblity issues in any new UAPI.
Thomas
^ permalink raw reply
* Re: [PATCH net-next v2 1/4] selftests: net: Move some UAPI header inclusions after libc ones
From: Matthieu Baerts @ 2026-01-30 10:39 UTC (permalink / raw)
To: Thomas Weißschuh
Cc: netdev, linux-kernel, linux-api, Arnd Bergmann, linux-kselftest,
mptcp, linux-security-module, bpf, libc-alpha
In-Reply-To: <20260130111949-4b4eb870-6df7-4026-a48c-730fa8660ad6@linutronix.de>
Hi Thomas,
On 30/01/2026 11:22, Thomas Weißschuh wrote:
(...)
>> Note that I just noticed this is the only file from this directory where
>> the "includes" are not sorted by type and alphabetical order, see
>> pm_nl_ctl.c as an example. A bit of a detail, but if you plan to send a
>> v2, do you mind doing that too here while at it, please?
>
> I'll send a v3 during the next cycle.
Thanks!
>> If not, I can look at that later, but better to avoid doing that in
>> parallel.
>
> If you want to fix this up already during this cycle,
> that would also be most welcome.
Sure, I can do that.
Cheers,
Matt
--
Sponsored by the NGI0 Core fund.
^ permalink raw reply
* Re: [PATCH] ipe: document AT_EXECVE_CHECK TOCTOU issue on OverlayFS
From: Amir Goldstein @ 2026-01-30 11:05 UTC (permalink / raw)
To: wufan
Cc: linux-security-module, linux-doc, linux-kernel, corbet, mic,
miklos, linux-unionfs
In-Reply-To: <20260130001418.18414-1-wufan@kernel.org>
On Fri, Jan 30, 2026 at 1:14 AM <wufan@kernel.org> wrote:
>
> From: Fan Wu <wufan@kernel.org>
>
> Document a known TOCTOU (time-of-check to time-of-use) issue when using
> AT_EXECVE_CHECK with read() on OverlayFS.
Hi Fan Wu,
TBH, I don't like the way that this problem is being framed.
IIUC, the problem is using IPE on a non-read-only fs.
Is that correct?
That fact that IPE metadata is usually coupled with read-only fs
is interesting for the understanding of the use case, but unless
IPE feature mandates read-only fs, this is a generic problem.
OverlayFS is just one private case, which happens to be common
in Android or containers? IDK, you did not mention this.
Please describe the problem as a generic problem and give
overlayfs as an example, preferable with references to the
real world use cases.
If I misunderstood, please explain why this problem is exclusive
to overlayfs.
Thanks,
Amir.
> The deny_write_access()
> protection is only held during the syscall, allowing a copy-up operation
> to be triggered afterward, causing subsequent read() calls to return
> content from the unprotected upper layer.
>
> This is generally not a concern for typical IPE deployments since
> dm-verity and fs-verity protected files are effectively read-only.
> However, OverlayFS with a writable upper layer presents a special case.
>
> Document mitigation strategies including mounting overlay as read-only
> and using mmap() instead of read(). Note that the mmap() mitigation
> relies on current OverlayFS implementation details and should not be
> considered a security guarantee.
>
> Signed-off-by: Fan Wu <wufan@kernel.org>
> ---
> Documentation/admin-guide/LSM/ipe.rst | 32 +++++++++++++++++++++++++++
> 1 file changed, 32 insertions(+)
>
> diff --git a/Documentation/admin-guide/LSM/ipe.rst b/Documentation/admin-guide/LSM/ipe.rst
> index a756d8158531..b621a98fe5e2 100644
> --- a/Documentation/admin-guide/LSM/ipe.rst
> +++ b/Documentation/admin-guide/LSM/ipe.rst
> @@ -110,6 +110,34 @@ intercepts during the execution process, this mechanism needs the interpreter
> to take the initiative, and existing interpreters won't be automatically
> supported unless the signal call is added.
>
> +.. WARNING::
> +
> + There is a known TOCTOU (time-of-check to time-of-use) issue with
> + ``AT_EXECVE_CHECK`` when interpreters use ``read()`` to obtain script
> + contents after the check [#atacexecvecheck_toctou]_. The ``AT_EXECVE_CHECK``
> + protection (via ``deny_write_access()``) is only held during the syscall.
> + After it returns, the file can be modified before the interpreter reads it.
> +
> + In typical IPE deployments, this is not a concern because files protected
> + by dm-verity or fs-verity are effectively read-only and cannot be modified.
> + However, OverlayFS presents a special case: when the lower layer is
> + dm-verity protected (read-only) but the upper layer is writable, an
> + attacker with write access can trigger a copy-up operation after the
> + ``AT_EXECVE_CHECK`` returns, causing subsequent ``read()`` calls to return
> + content from the unprotected upper layer instead of the verified lower layer.
> +
> + To mitigate this issue on OverlayFS:
> +
> + - Mount the overlay as read-only, or restrict write access to the upper
> + layer.
> + - Interpreters may use ``mmap()`` instead of ``read()`` to obtain script
> + contents. Currently, OverlayFS fixes the underlying real file reference
> + at ``open()`` time for mmap operations, so mmap will continue to access
> + the original lower layer file even after a copy-up. However, this
> + behavior is an implementation detail of OverlayFS and is not guaranteed
> + to remain stable across kernel versions. Do not rely on this as a
> + security guarantee.
> +
> Threat Model
> ------------
>
> @@ -833,3 +861,7 @@ A:
> kernel's fsverity support; IPE does not impose any
> restrictions on the digest algorithm itself;
> thus, this list may be out of date.
> +
> +.. [#atacexecvecheck_toctou] See the O_DENY_WRITE RFC discussion for details on
> + this TOCTOU issue:
> + https://lore.kernel.org/all/20250822170800.2116980-1-mic@digikod.net/
> --
> 2.52.0
>
^ permalink raw reply
* Re: [PATCH v4] ima: Fallback to ctime check for FS without kstat.change_cookie
From: Mimi Zohar @ 2026-01-30 13:00 UTC (permalink / raw)
To: Frederick Lawler, Roberto Sassu, Dmitry Kasatkin, Eric Snowberg,
Paul Moore, James Morris, Serge E. Hallyn, Darrick J. Wong,
Christian Brauner, Josef Bacik, Jeff Layton
Cc: linux-kernel, linux-integrity, linux-security-module, kernel-team
In-Reply-To: <42da6a2c72b1093b7aa5106cdce4d0efdbd785ee.camel@linux.ibm.com>
On Thu, 2026-01-29 at 21:32 -0500, Mimi Zohar wrote:
> On Thu, 2026-01-29 at 12:07 -0600, Frederick Lawler wrote:
> > Commit 1cf7e834a6fb ("xfs: switch to multigrain timestamps")
> > introduced a means to track change detection for an inode
> > via ctime updates, opposed to setting kstat.change_cookie to
> > an i_version when calling into xfs_vn_getattr().
> >
> > This introduced a regression for IMA such that an action
> > performed on a LOWER inode on a stacked file systems always
> > requires a re-evaluation if the LOWER file system does not
> > leverage kstat.change_cookie to track inode i_version or lacks
> > i_version support all together.
>
> Please describe the change in behavior that needs to be fixed. Are there too
> many, too few measurements, or both?
>
> Examples are fine, but first describe the problem - not detecting file change on
> xfs.
>
> >
> > In the case of stacking XFS on XFS, an action on either the LOWER or UPPER
> > will require re-evaluation. Stacking TMPFS on XFS for instance, once the
> > inode is UPPER is mutated, IMA resumes normal behavior because TMPFS
> > leverages generic_fillattr() to update the change cookie.
>
> This sounds like the same issue - not detecting file change on xfs. The problem
> is simply manifesting itself on stacked filesystems.
Splitting this patch, so that the base XFS changes are in one patch and the
stacked filesystem changes are in the other, would really help clarify what is
needed and the reason why.
Mimi
>
> >
> > This is because IMA caches kstat.change_cookie to compare against an
> > inode's i_version directly in integrity_inode_attrs_changed(), and thus
> > could be out of date depending on how file systems set
> > kstat.change_cookie.
> >
> > To address this, require integrity_inode_attrs_changed() to query
> > vfs_getattr_nosec() to compare the cached version against
> > kstat.change_cookie directly. This ensures that when updates occur,
> > we're accessing the same changed inode version on changes, and fallback
> > to compare against kstat.ctime when STATX_CHANGE_COOKIE is missing from
> > result mask.
> >
> > Lastly, because EVM still relies on querying and caching a inode's
> > i_version directly, the integrity_inode_attrs_changed() falls back to the
> > original inode.i_version != cached comparison.
> >
> > Link: https://lore.kernel.org/all/aTspr4_h9IU4EyrR@CMGLRV3
> > Fixes: 1cf7e834a6fb ("xfs: switch to multigrain timestamps")
> > Suggested-by: Jeff Layton <jlayton@kernel.org>
> > Reviewed-by: Jeff Layton <jlayton@kernel.org>
> > Signed-off-by: Frederick Lawler <fred@cloudflare.com>
> > ---
> > include/linux/integrity.h | 35 +++++++++++++++++++++++++++++++----
> > security/integrity/evm/evm_main.c | 5 ++---
> > security/integrity/ima/ima_api.c | 11 ++++++++---
> > security/integrity/ima/ima_main.c | 17 ++++++++++-------
> > 4 files changed, 51 insertions(+), 17 deletions(-)
> >
> > diff --git a/include/linux/integrity.h b/include/linux/integrity.h
> > index f5842372359be5341b6870a43b92e695e8fc78af..034f0a1ed48ca8c19c764e302bbfc555dad92cde 100644
> > --- a/include/linux/integrity.h
> > +++ b/include/linux/integrity.h
> > @@ -9,6 +9,8 @@
> >
> > #include <linux/fs.h>
> > #include <linux/iversion.h>
> > +#include <linux/kernel.h>
> > +#include <linux/time64.h>
> >
> > enum integrity_status {
> > INTEGRITY_PASS = 0,
> > @@ -51,14 +53,39 @@ integrity_inode_attrs_store(struct integrity_inode_attributes *attrs,
> >
> > /*
> > * On stacked filesystems detect whether the inode or its content has changed.
> > + *
> > + * Must be called in process context.
> > */
> > static inline bool
> > integrity_inode_attrs_changed(const struct integrity_inode_attributes *attrs,
> > - const struct inode *inode)
> > + struct file *file, struct inode *inode)
> > {
> > - return (inode->i_sb->s_dev != attrs->dev ||
> > - inode->i_ino != attrs->ino ||
> > - !inode_eq_iversion(inode, attrs->version));
> > + struct kstat stat;
> > +
> > + might_sleep();
> > +
> > + if (inode->i_sb->s_dev != attrs->dev || inode->i_ino != attrs->ino)
> > + return true;
> > +
> > + /*
> > + * EVM currently relies on backing inode i_version. While IS_I_VERSION
> > + * is not a good indicator of i_version support, this still retains
> > + * the logic such that a re-evaluation should still occur for EVM, and
> > + * only for IMA if vfs_getattr_nosec() fails.
> > + */
> > + if (!file || vfs_getattr_nosec(&file->f_path, &stat,
> > + STATX_CHANGE_COOKIE | STATX_CTIME,
> > + AT_STATX_SYNC_AS_STAT))
> > + return !IS_I_VERSION(inode) ||
> > + !inode_eq_iversion(inode, attrs->version);
> > +
> > + if (stat.result_mask & STATX_CHANGE_COOKIE)
> > + return stat.change_cookie != attrs->version;
> > +
> > + if (stat.result_mask & STATX_CTIME)
> > + return timespec64_to_ns(&stat.ctime) != (s64)attrs->version;
> > +
> > + return true;
> > }
> >
> >
> > diff --git a/security/integrity/ima/ima_main.c b/security/integrity/ima/ima_main.c
> > index 5770cf691912aa912fc65280c59f5baac35dd725..8ac42b03740eb93bf23b15cb9039af6cd32aa999 100644
> > --- a/security/integrity/ima/ima_main.c
> > +++ b/security/integrity/ima/ima_main.c
> > @@ -28,6 +28,7 @@
> > #include <linux/iversion.h>
> > #include <linux/evm.h>
> > #include <linux/crash_dump.h>
> > +#include <linux/time64.h>
> >
> > #include "ima.h"
> >
> > @@ -199,10 +200,13 @@ static void ima_check_last_writer(struct ima_iint_cache *iint,
> > &iint->atomic_flags);
> > if ((iint->flags & IMA_NEW_FILE) ||
> > vfs_getattr_nosec(&file->f_path, &stat,
> > - STATX_CHANGE_COOKIE,
> > - AT_STATX_SYNC_AS_STAT) ||
> > - !(stat.result_mask & STATX_CHANGE_COOKIE) ||
> > - stat.change_cookie != iint->real_inode.version) {
> > + STATX_CHANGE_COOKIE | STATX_CTIME,
> > + AT_STATX_SYNC_AS_STAT) ||
> > + ((stat.result_mask & STATX_CHANGE_COOKIE) ?
> > + stat.change_cookie != iint->real_inode.version :
> > + (!(stat.result_mask & STATX_CTIME) ||
> > + timespec64_to_ns(&stat.ctime) !=
> > + (s64)iint->real_inode.version))) {
> > iint->flags &= ~(IMA_DONE_MASK | IMA_NEW_FILE);
> > iint->measured_pcrs = 0;
> > if (update)
>
> The original i_version test was clear. This code has become really hard to
> understand and needs to be cleaned up. Defining a helper function, will also
> avoid code duplication.
>
> Mimi
^ permalink raw reply
* Re: [PATCH net-next v2 0/4] net: uapi: Provide an UAPI definition of 'struct sockaddr'
From: Jakub Kicinski @ 2026-01-30 16:17 UTC (permalink / raw)
To: Thomas Weißschuh
Cc: Eric Dumazet, Kuniyuki Iwashima, Paolo Abeni, Willem de Bruijn,
David S. Miller, Simon Horman, Shuah Khan, Matthieu Baerts,
Mat Martineau, Geliang Tang, Mickaël Salaün,
Günther Noack, Alexei Starovoitov, Daniel Borkmann,
Jesper Dangaard Brouer, John Fastabend, Stanislav Fomichev,
Andrii Nakryiko, Martin KaFai Lau, Eduard Zingerman, Song Liu,
Yonghong Song, KP Singh, Hao Luo, Jiri Olsa, netdev, linux-kernel,
linux-api, Arnd Bergmann, linux-kselftest, mptcp,
linux-security-module, bpf, libc-alpha, Carlos O'Donell,
Adhemerval Zanella, Rich Felker, klibc, Florian Weimer
In-Reply-To: <20260130112309-28f2645b-e756-4173-96da-cf5c59191520@linutronix.de>
On Fri, 30 Jan 2026 11:34:15 +0100 Thomas Weißschuh wrote:
> > Are all those selftests / samples getting broken by this patch set?
>
> Yes.
>
> Some of them get broken by the new 'struct sockaddr', but some others are
> already broken just by the new transitive inclusion of libc-compat.h.
> So any header starting to use the compatibility machinery may trigger breakage
> in code including UAPI headers before libc header, even for completely new type
> definitions which themselves would not conflict with libc.
Let's split the uAPI header changes from any selftest changes.
If you're saying the the selftests no longer build after the uAPI
header changes then of course we can't apply the patches.
^ permalink raw reply
* Re: [PATCH] lsm: preserve /proc/sys/vm/mmap_min_addr when !CONFIG_SECURITY
From: Paul Moore @ 2026-01-30 16:48 UTC (permalink / raw)
To: Kees Cook; +Cc: linux-security-module, linux-mm, lorenzo.stoakes
In-Reply-To: <202601291730.45120C1A@keescook>
On Thu, Jan 29, 2026 at 8:31 PM Kees Cook <kees@kernel.org> wrote:
> On Thu, Jan 29, 2026 at 05:51:33PM -0500, Paul Moore wrote:
> > While reworking the LSM initialization code the
> > /proc/sys/vm/mmap_min_addr handler was inadvertently caught up in the
> > change and the procfs entry wasn't setup when CONFIG_SECURITY was not
> > selected at kernel build time. This patch restores the previous behavior
> > and ensures that the procfs entry is setup regardless of the
> > CONFIG_SECURITY state.
> >
> > Future work will improve upon this, likely by moving the procfs handler
> > into the mm subsystem, but this patch should resolve the immediate
> > regression.
> >
> > Fixes: 4ab5efcc2829 ("lsm: consolidate all of the LSM framework initcalls")
> > Reported-by: Lorenzo Stoakes <lorenzo.stoakes@oracle.com>
> > Signed-off-by: Paul Moore <paul@paul-moore.com>
>
> Good catch and fix!
>
> Reviewed-by: Kees Cook <kees@kernel.org>
Merged into lsm/stable-6.19, with plans to send this to Linus early
next week after a day or two in linux-next. Thanks everyone!
--
paul-moore.com
^ permalink raw reply
* Re: [PATCH v4 15/17] module: Introduce hash-based integrity checking
From: Petr Pavlu @ 2026-01-30 17:06 UTC (permalink / raw)
To: Thomas Weißschuh
Cc: Nathan Chancellor, Arnd Bergmann, Luis Chamberlain, Sami Tolvanen,
Daniel Gomez, Paul Moore, James Morris, Serge E. Hallyn,
Jonathan Corbet, Madhavan Srinivasan, Michael Ellerman,
Nicholas Piggin, Naveen N Rao, Mimi Zohar, Roberto Sassu,
Dmitry Kasatkin, Eric Snowberg, Nicolas Schier, Daniel Gomez,
Aaron Tomlin, Christophe Leroy (CS GROUP), Nicolas Schier,
Nicolas Bouchinet, Xiu Jianfeng, Fabian Grünbichler,
Arnout Engelen, Mattia Rizzolo, kpcyrd, Christian Heusel,
Câju Mihai-Drosi, Sebastian Andrzej Siewior, linux-kbuild,
linux-kernel, linux-arch, linux-modules, linux-security-module,
linux-doc, linuxppc-dev, linux-integrity
In-Reply-To: <20260113-module-hashes-v4-15-0b932db9b56b@weissschuh.net>
On 1/13/26 1:28 PM, Thomas Weißschuh wrote:
> Normally the .ko module files depend on a fully built vmlinux to be
> available for modpost validation and BTF generation. With
> CONFIG_MODULE_HASHES, vmlinux now depends on the modules
> to build a merkle tree. This introduces a dependency cycle which is
> impossible to satisfy. Work around this by building the modules during
> link-vmlinux.sh, after vmlinux is complete enough for modpost and BTF
> but before the final module hashes are
I wonder if this dependency cycle could be resolved by utilizing the
split into vmlinux.unstripped and vmlinux that occurred last year.
The idea is to create the following ordering: vmlinux.unstripped ->
modules -> vmlinux, and to patch in .module_hashes only when building
the final vmlinux.
This would require the following:
* Split scripts/Makefile.vmlinux into two Makefiles, one that builds the
current vmlinux.unstripped and the second one that builds the final
vmlinux from it.
* Modify the top Makefile to recognize vmlinux.unstripped and update the
BTF generation rule 'modules: vmlinux' to
'modules: vmlinux.unstripped'.
* Add the 'vmlinux: modules' ordering in the top Makefile for
CONFIG_MODULE_HASHES=y.
* Remove the patching of vmlinux.unstripped in scripts/link-vmlinux.sh
and instead move it into scripts/Makefile.vmlinux when running objcopy
to produce the final vmlinux.
I think this approach has two main advantages:
* CONFIG_MODULE_HASHES can be made orthogonal to
CONFIG_DEBUG_INFO_BTF_MODULES.
* All dependencies are expressed at the Makefile level instead of having
scripts/link-vmlinux.sh invoke 'make -f Makefile modules'.
Below is a rough prototype that applies on top of this series. It is a
bit verbose due to the splitting of part of scripts/Makefile.vmlinux
into scripts/Makefile.vmlinux_unstripped.
--
Thanks,
Petr
diff --git a/Makefile b/Makefile
index 841772a5a260..19a3beb82fa7 100644
--- a/Makefile
+++ b/Makefile
@@ -1259,7 +1259,7 @@ vmlinux_o: vmlinux.a $(KBUILD_VMLINUX_LIBS)
vmlinux.o modules.builtin.modinfo modules.builtin: vmlinux_o
@:
-PHONY += vmlinux
+PHONY += vmlinux.unstripped vmlinux
# LDFLAGS_vmlinux in the top Makefile defines linker flags for the top vmlinux,
# not for decompressors. LDFLAGS_vmlinux in arch/*/boot/compressed/Makefile is
# unrelated; the decompressors just happen to have the same base name,
@@ -1270,9 +1270,11 @@ PHONY += vmlinux
# https://savannah.gnu.org/bugs/?61463
# For Make > 4.4, the following simple code will work:
# vmlinux: private export LDFLAGS_vmlinux := $(LDFLAGS_vmlinux)
-vmlinux: private _LDFLAGS_vmlinux := $(LDFLAGS_vmlinux)
-vmlinux: export LDFLAGS_vmlinux = $(_LDFLAGS_vmlinux)
-vmlinux: vmlinux.o $(KBUILD_LDS) modpost
+vmlinux.unstripped: private _LDFLAGS_vmlinux := $(LDFLAGS_vmlinux)
+vmlinux.unstripped: export LDFLAGS_vmlinux = $(_LDFLAGS_vmlinux)
+vmlinux.unstripped: vmlinux.o $(KBUILD_LDS) modpost
+ $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.vmlinux_unstripped
+vmlinux: vmlinux.unstripped
$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.vmlinux
# The actual objects are generated when descending,
@@ -1541,7 +1543,7 @@ all: dtbs
endif
ifdef CONFIG_GENERIC_BUILTIN_DTB
-vmlinux: dtbs
+vmlinux.unstripped: dtbs
endif
endif
@@ -1588,9 +1590,11 @@ endif
# is an exception.
ifdef CONFIG_DEBUG_INFO_BTF_MODULES
KBUILD_BUILTIN := y
-ifndef CONFIG_MODULE_HASHES
-modules: vmlinux
+modules: vmlinux.unstripped
endif
+
+ifdef CONFIG_MODULE_HASHES
+vmlinux: modules
endif
modules: modules_prepare
@@ -1983,11 +1987,7 @@ modules.order: $(build-dir)
# KBUILD_MODPOST_NOFINAL can be set to skip the final link of modules.
# This is solely useful to speed up test compiles.
modules: modpost
-ifdef CONFIG_MODULE_HASHES
-ifeq ($(MODULE_HASHES_MODPOST_FINAL), 1)
- $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modfinal
-endif
-else ifneq ($(KBUILD_MODPOST_NOFINAL),1)
+ifneq ($(KBUILD_MODPOST_NOFINAL),1)
$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modfinal
endif
diff --git a/scripts/Makefile.modfinal b/scripts/Makefile.modfinal
index 890724edac69..213e21ecfe0d 100644
--- a/scripts/Makefile.modfinal
+++ b/scripts/Makefile.modfinal
@@ -55,7 +55,7 @@ if_changed_except = $(if $(call newer_prereqs_except,$(2))$(cmd-check), \
$(cmd); \
printf '%s\n' 'savedcmd_$@ := $(make-cmd)' > $(dot-target).cmd, @:)
-# Re-generate module BTFs if either module's .ko or vmlinux changed
+# Re-generate module BTFs if either module's .ko or vmlinux.unstripped changed
%.ko: %.o %.mod.o .module-common.o $(objtree)/scripts/module.lds $(and $(CONFIG_DEBUG_INFO_BTF_MODULES),$(KBUILD_BUILTIN),$(objtree)/.tmp_vmlinux_btf.stamp) FORCE
+$(call if_changed_except,ld_ko_o,$(objtree)/.tmp_vmlinux_btf.stamp)
ifdef CONFIG_DEBUG_INFO_BTF_MODULES
diff --git a/scripts/Makefile.vmlinux b/scripts/Makefile.vmlinux
index 4ce849f6253a..8c2a938c88ab 100644
--- a/scripts/Makefile.vmlinux
+++ b/scripts/Makefile.vmlinux
@@ -15,78 +15,24 @@ targets :=
%.o: %.S FORCE
$(call if_changed_rule,as_o_S)
-# Built-in dtb
-# ---------------------------------------------------------------------------
-
-quiet_cmd_wrap_dtbs = WRAP $@
- cmd_wrap_dtbs = { \
- echo '\#include <asm-generic/vmlinux.lds.h>'; \
- echo '.section .dtb.init.rodata,"a"'; \
- while read dtb; do \
- symbase=__dtb_$$(basename -s .dtb "$${dtb}" | tr - _); \
- echo '.balign STRUCT_ALIGNMENT'; \
- echo ".global $${symbase}_begin"; \
- echo "$${symbase}_begin:"; \
- echo '.incbin "'$$dtb'" '; \
- echo ".global $${symbase}_end"; \
- echo "$${symbase}_end:"; \
- done < $<; \
- } > $@
-
-.builtin-dtbs.S: .builtin-dtbs-list FORCE
- $(call if_changed,wrap_dtbs)
-
-quiet_cmd_gen_dtbs_list = GEN $@
- cmd_gen_dtbs_list = \
- $(if $(CONFIG_BUILTIN_DTB_NAME), echo "arch/$(SRCARCH)/boot/dts/$(CONFIG_BUILTIN_DTB_NAME).dtb",:) > $@
-
-.builtin-dtbs-list: arch/$(SRCARCH)/boot/dts/dtbs-list FORCE
- $(call if_changed,$(if $(CONFIG_BUILTIN_DTB_ALL),copy,gen_dtbs_list))
-
-targets += .builtin-dtbs-list
-
-ifdef CONFIG_GENERIC_BUILTIN_DTB
-targets += .builtin-dtbs.S .builtin-dtbs.o
-vmlinux.unstripped: .builtin-dtbs.o
-endif
-
-# vmlinux.unstripped
+# vmlinux
# ---------------------------------------------------------------------------
-ifdef CONFIG_ARCH_WANTS_PRE_LINK_VMLINUX
-vmlinux.unstripped: arch/$(SRCARCH)/tools/vmlinux.arch.o
-
-arch/$(SRCARCH)/tools/vmlinux.arch.o: vmlinux.o FORCE
- $(Q)$(MAKE) $(build)=arch/$(SRCARCH)/tools $@
-endif
-
-ARCH_POSTLINK := $(wildcard $(srctree)/arch/$(SRCARCH)/Makefile.postlink)
-
-# Final link of vmlinux with optional arch pass after final link
-cmd_link_vmlinux = \
- $< "$(LD)" "$(KBUILD_LDFLAGS)" "$(LDFLAGS_vmlinux)" "$@"; \
- $(if $(ARCH_POSTLINK), $(MAKE) -f $(ARCH_POSTLINK) $@, true)
+ifdef CONFIG_MODULE_HASHES
+targets += .tmp_module_hashes.o
+.tmp_module_hashes.o: .tmp_module_hashes.c FORCE
-targets += vmlinux.unstripped .vmlinux.export.o
-vmlinux.unstripped: scripts/link-vmlinux.sh vmlinux.o .vmlinux.export.o $(KBUILD_LDS) FORCE
- +$(call if_changed_dep,link_vmlinux)
-ifdef CONFIG_DEBUG_INFO_BTF
-vmlinux.unstripped: $(RESOLVE_BTFIDS)
-endif
+quiet_cmd_module_hashes = OBJCOPY $@
+ cmd_module_hashes = $(OBJCOPY) --dump-section .module_hashes=$@ $<
-ifdef CONFIG_BUILDTIME_TABLE_SORT
-vmlinux.unstripped: scripts/sorttable
-endif
+targets += .tmp_module_hashes.bin
+.tmp_module_hashes.bin: .tmp_module_hashes.o FORCE
+ $(call if_changed,module_hashes)
-ifdef CONFIG_MODULE_HASHES
-vmlinux.unstripped: $(objtree)/scripts/modules-merkle-tree
-vmlinux.unstripped: modules.order
-vmlinux.unstripped: $(wildcard include/config/MODULE_INSTALL_STRIP)
+vmlinux: .tmp_module_hashes.bin
+patch-module-hashes := --update-section .module_hashes=.tmp_module_hashes.bin
endif
-# vmlinux
-# ---------------------------------------------------------------------------
-
remove-section-y := .modinfo
remove-section-$(CONFIG_ARCH_VMLINUX_NEEDS_RELOCS) += '.rel*' '!.rel*.dyn'
# for compatibility with binutils < 2.32
@@ -98,70 +44,15 @@ remove-symbols := -w --strip-unneeded-symbol='__mod_device_table__*'
# To avoid warnings: "empty loadable segment detected at ..." from GNU objcopy,
# it is necessary to remove the PT_LOAD flag from the segment.
quiet_cmd_strip_relocs = OBJCOPY $@
- cmd_strip_relocs = $(OBJCOPY) $(patsubst %,--set-section-flags %=noload,$(remove-section-y)) $< $@; \
- $(OBJCOPY) $(addprefix --remove-section=,$(remove-section-y)) $(remove-symbols) $@
+ cmd_script_relocs = $(OBJCOPY) $(patsubst %,--set-section-flags %=noload,$(remove-section-y)) $< $@; \
+ $(OBJCOPY) $(addprefix --remove-section=,$(remove-section-y)) \
+ $(remove-symbols) \
+ $(patch-module-hashes) $@
targets += vmlinux
vmlinux: vmlinux.unstripped FORCE
$(call if_changed,strip_relocs)
-# modules.builtin.modinfo
-# ---------------------------------------------------------------------------
-
-# .modinfo in vmlinux.unstripped is aligned to 8 bytes for compatibility with
-# tools that expect vmlinux to have sufficiently aligned sections but the
-# additional bytes used for padding .modinfo to satisfy this requirement break
-# certain versions of kmod with
-#
-# depmod: ERROR: kmod_builtin_iter_next: unexpected string without modname prefix
-#
-# Strip the trailing padding bytes after extracting .modinfo to comply with
-# what kmod expects to parse.
-quiet_cmd_modules_builtin_modinfo = GEN $@
- cmd_modules_builtin_modinfo = $(cmd_objcopy); \
- sed -i 's/\x00\+$$/\x00/g' $@
-
-OBJCOPYFLAGS_modules.builtin.modinfo := -j .modinfo -O binary
-
-targets += modules.builtin.modinfo
-modules.builtin.modinfo: vmlinux.unstripped FORCE
- $(call if_changed,modules_builtin_modinfo)
-
-# modules.builtin
-# ---------------------------------------------------------------------------
-
-__default: modules.builtin
-
-# The second line aids cases where multiple modules share the same object.
-
-quiet_cmd_modules_builtin = GEN $@
- cmd_modules_builtin = \
- tr '\0' '\n' < $< | \
- sed -n 's/^[[:alnum:]:_]*\.file=//p' | \
- tr ' ' '\n' | uniq | sed -e 's:^:kernel/:' -e 's/$$/.ko/' > $@
-
-targets += modules.builtin
-modules.builtin: modules.builtin.modinfo FORCE
- $(call if_changed,modules_builtin)
-
-# modules.builtin.ranges
-# ---------------------------------------------------------------------------
-ifdef CONFIG_BUILTIN_MODULE_RANGES
-__default: modules.builtin.ranges
-
-quiet_cmd_modules_builtin_ranges = GEN $@
- cmd_modules_builtin_ranges = gawk -f $(real-prereqs) > $@
-
-targets += modules.builtin.ranges
-modules.builtin.ranges: $(srctree)/scripts/generate_builtin_ranges.awk \
- modules.builtin vmlinux.map vmlinux.o.map FORCE
- $(call if_changed,modules_builtin_ranges)
-
-vmlinux.map: vmlinux.unstripped
- @:
-
-endif
-
# Add FORCE to the prerequisites of a target to force it to be always rebuilt.
# ---------------------------------------------------------------------------
diff --git a/scripts/Makefile.vmlinux_unstripped b/scripts/Makefile.vmlinux_unstripped
new file mode 100644
index 000000000000..914ee6f3b935
--- /dev/null
+++ b/scripts/Makefile.vmlinux_unstripped
@@ -0,0 +1,159 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+PHONY := __default
+__default: vmlinux.unstripped
+
+include include/config/auto.conf
+include $(srctree)/scripts/Kbuild.include
+include $(srctree)/scripts/Makefile.lib
+
+targets :=
+
+%.o: %.c FORCE
+ $(call if_changed_rule,cc_o_c)
+
+%.o: %.S FORCE
+ $(call if_changed_rule,as_o_S)
+
+# Built-in dtb
+# ---------------------------------------------------------------------------
+
+quiet_cmd_wrap_dtbs = WRAP $@
+ cmd_wrap_dtbs = { \
+ echo '\#include <asm-generic/vmlinux.lds.h>'; \
+ echo '.section .dtb.init.rodata,"a"'; \
+ while read dtb; do \
+ symbase=__dtb_$$(basename -s .dtb "$${dtb}" | tr - _); \
+ echo '.balign STRUCT_ALIGNMENT'; \
+ echo ".global $${symbase}_begin"; \
+ echo "$${symbase}_begin:"; \
+ echo '.incbin "'$$dtb'" '; \
+ echo ".global $${symbase}_end"; \
+ echo "$${symbase}_end:"; \
+ done < $<; \
+ } > $@
+
+.builtin-dtbs.S: .builtin-dtbs-list FORCE
+ $(call if_changed,wrap_dtbs)
+
+quiet_cmd_gen_dtbs_list = GEN $@
+ cmd_gen_dtbs_list = \
+ $(if $(CONFIG_BUILTIN_DTB_NAME), echo "arch/$(SRCARCH)/boot/dts/$(CONFIG_BUILTIN_DTB_NAME).dtb",:) > $@
+
+.builtin-dtbs-list: arch/$(SRCARCH)/boot/dts/dtbs-list FORCE
+ $(call if_changed,$(if $(CONFIG_BUILTIN_DTB_ALL),copy,gen_dtbs_list))
+
+targets += .builtin-dtbs-list
+
+ifdef CONFIG_GENERIC_BUILTIN_DTB
+targets += .builtin-dtbs.S .builtin-dtbs.o
+vmlinux.unstripped: .builtin-dtbs.o
+endif
+
+# vmlinux.unstripped
+# ---------------------------------------------------------------------------
+
+ifdef CONFIG_ARCH_WANTS_PRE_LINK_VMLINUX
+vmlinux.unstripped: arch/$(SRCARCH)/tools/vmlinux.arch.o
+
+arch/$(SRCARCH)/tools/vmlinux.arch.o: vmlinux.o FORCE
+ $(Q)$(MAKE) $(build)=arch/$(SRCARCH)/tools $@
+endif
+
+ARCH_POSTLINK := $(wildcard $(srctree)/arch/$(SRCARCH)/Makefile.postlink)
+
+# Final link of vmlinux with optional arch pass after final link
+cmd_link_vmlinux = \
+ $< "$(LD)" "$(KBUILD_LDFLAGS)" "$(LDFLAGS_vmlinux)" "$@"; \
+ $(if $(ARCH_POSTLINK), $(MAKE) -f $(ARCH_POSTLINK) $@, true)
+
+targets += vmlinux.unstripped .vmlinux.export.o
+vmlinux.unstripped: scripts/link-vmlinux.sh vmlinux.o .vmlinux.export.o $(KBUILD_LDS) FORCE
+ +$(call if_changed_dep,link_vmlinux)
+ifdef CONFIG_DEBUG_INFO_BTF
+vmlinux.unstripped: $(RESOLVE_BTFIDS)
+endif
+
+ifdef CONFIG_BUILDTIME_TABLE_SORT
+vmlinux.unstripped: scripts/sorttable
+endif
+
+ifdef CONFIG_MODULE_HASHES
+vmlinux.unstripped: $(objtree)/scripts/modules-merkle-tree
+vmlinux.unstripped: modules.order
+vmlinux.unstripped: $(wildcard include/config/MODULE_INSTALL_STRIP)
+endif
+
+# modules.builtin.modinfo
+# ---------------------------------------------------------------------------
+
+# .modinfo in vmlinux.unstripped is aligned to 8 bytes for compatibility with
+# tools that expect vmlinux to have sufficiently aligned sections but the
+# additional bytes used for padding .modinfo to satisfy this requirement break
+# certain versions of kmod with
+#
+# depmod: ERROR: kmod_builtin_iter_next: unexpected string without modname prefix
+#
+# Strip the trailing padding bytes after extracting .modinfo to comply with
+# what kmod expects to parse.
+quiet_cmd_modules_builtin_modinfo = GEN $@
+ cmd_modules_builtin_modinfo = $(cmd_objcopy); \
+ sed -i 's/\x00\+$$/\x00/g' $@
+
+OBJCOPYFLAGS_modules.builtin.modinfo := -j .modinfo -O binary
+
+targets += modules.builtin.modinfo
+modules.builtin.modinfo: vmlinux.unstripped FORCE
+ $(call if_changed,modules_builtin_modinfo)
+
+# modules.builtin
+# ---------------------------------------------------------------------------
+
+__default: modules.builtin
+
+# The second line aids cases where multiple modules share the same object.
+
+quiet_cmd_modules_builtin = GEN $@
+ cmd_modules_builtin = \
+ tr '\0' '\n' < $< | \
+ sed -n 's/^[[:alnum:]:_]*\.file=//p' | \
+ tr ' ' '\n' | uniq | sed -e 's:^:kernel/:' -e 's/$$/.ko/' > $@
+
+targets += modules.builtin
+modules.builtin: modules.builtin.modinfo FORCE
+ $(call if_changed,modules_builtin)
+
+# modules.builtin.ranges
+# ---------------------------------------------------------------------------
+ifdef CONFIG_BUILTIN_MODULE_RANGES
+__default: modules.builtin.ranges
+
+quiet_cmd_modules_builtin_ranges = GEN $@
+ cmd_modules_builtin_ranges = gawk -f $(real-prereqs) > $@
+
+targets += modules.builtin.ranges
+modules.builtin.ranges: $(srctree)/scripts/generate_builtin_ranges.awk \
+ modules.builtin vmlinux.map vmlinux.o.map FORCE
+ $(call if_changed,modules_builtin_ranges)
+
+vmlinux.map: vmlinux.unstripped
+ @:
+
+endif
+
+# Add FORCE to the prerequisites of a target to force it to be always rebuilt.
+# ---------------------------------------------------------------------------
+
+PHONY += FORCE
+FORCE:
+
+# Read all saved command lines and dependencies for the $(targets) we
+# may be building above, using $(if_changed{,_dep}). As an
+# optimization, we don't need to read them if the target does not
+# exist, we will rebuild anyway in that case.
+
+existing-targets := $(wildcard $(sort $(targets)))
+
+-include $(foreach f,$(existing-targets),$(dir $(f)).$(notdir $(f)).cmd)
+
+.PHONY: $(PHONY)
diff --git a/scripts/link-vmlinux.sh b/scripts/link-vmlinux.sh
index bfeff1f5753d..80cb09707426 100755
--- a/scripts/link-vmlinux.sh
+++ b/scripts/link-vmlinux.sh
@@ -316,17 +316,6 @@ if is_enabled CONFIG_BUILDTIME_TABLE_SORT; then
fi
fi
-if is_enabled CONFIG_MODULE_HASHES; then
- info MAKE modules
- ${MAKE} -f Makefile MODULE_HASHES_MODPOST_FINAL=1 modules
- module_hashes_o=.tmp_module_hashes.o
- info CC ${module_hashes_o}
- ${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} ${KBUILD_CFLAGS} \
- ${KBUILD_CFLAGS_KERNEL} -fno-lto -c -o "${module_hashes_o}" ".tmp_module_hashes.c"
- ${OBJCOPY} --dump-section .module_hashes=.tmp_module_hashes.bin ${module_hashes_o}
- ${OBJCOPY} --update-section .module_hashes=.tmp_module_hashes.bin ${VMLINUX}
-fi
-
# step a (see comment above)
if is_enabled CONFIG_KALLSYMS; then
if ! cmp -s System.map "${kallsyms_sysmap}"; then
^ permalink raw reply related
* Re: [PATCH] ipe: document AT_EXECVE_CHECK TOCTOU issue on OverlayFS
From: Fan Wu @ 2026-01-30 19:21 UTC (permalink / raw)
To: Amir Goldstein
Cc: wufan, linux-security-module, linux-doc, linux-kernel, corbet,
mic, miklos, linux-unionfs
In-Reply-To: <CAOQ4uxhf6EQKcoN055xzmi-RW2GPxRzz_ExsQawGQBSmoX2WYg@mail.gmail.com>
On Fri, Jan 30, 2026 at 3:06 AM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Fri, Jan 30, 2026 at 1:14 AM <wufan@kernel.org> wrote:
> >
> > From: Fan Wu <wufan@kernel.org>
> >
> > Document a known TOCTOU (time-of-check to time-of-use) issue when using
> > AT_EXECVE_CHECK with read() on OverlayFS.
>
> Hi Fan Wu,
>
> TBH, I don't like the way that this problem is being framed.
> IIUC, the problem is using IPE on a non-read-only fs.
> Is that correct?
>
> That fact that IPE metadata is usually coupled with read-only fs
> is interesting for the understanding of the use case, but unless
> IPE feature mandates read-only fs, this is a generic problem.
>
> OverlayFS is just one private case, which happens to be common
> in Android or containers? IDK, you did not mention this.
>
> Please describe the problem as a generic problem and give
> overlayfs as an example, preferable with references to the
> real world use cases.
>
> If I misunderstood, please explain why this problem is exclusive
> to overlayfs.
>
> Thanks,
> Amir.
>
Hi Amir,
Thanks for the review. That's exactly why we CC'd you and the
overlayfs folks, we wanted to get your perspective before documenting
this.
Let me give some background. IPE enforces execution policy based on
file integrity properties, primarily dm-verity and fs-verity. These
are the trust anchors, and files without these protections won't be
trusted by IPE. Since dm-verity and fs-verity are inherently
read-only, in typical deployments the TOCTOU issue doesn't exist. To
support overlayfs, IPE uses d_real_inode() to look through the overlay
and get the real inode from the lower layer.
Recently a new feature AT_EXECVE_CHECK was introduced to allow script
interpreters to request LSM checks on script files before execution.
The idea is: interpreter opens the script, calls execveat() with
AT_EXECVE_CHECK to verify the file passes security policy, then reads
and executes the content.
What we found is that on overlayfs with a dm-verity lower layer and
writable upper layer, when a script file only exists in the lower
layer, AT_EXECVE_CHECK passes because IPE sees it's dm-verity
protected. But if another process writes to the same path after
execveat() returns, copy-up happens and subsequent read() from the
original fd returns content from the upper layer. We verified this
through testing.
Overlayfs is popular in container environments, so we want to document
this for IPE users.
We noticed the overlayfs documentation
(https://docs.kernel.org/filesystems/overlayfs.html#non-standard-behavior)
states that if a lower layer file is opened and memory mapped,
subsequent changes are not reflected in the memory mapping. We also
verified this: mmap keeps the original lower layer content after
copy-up. One reason we CC'd you is to ask: is relying on mmap to keep
the original lower file reference a reasonable choice? Or would you
recommend against depending on this behavior?
The narrative in the patch can definitely be adjusted. Would something
like this work better:
"When using AT_EXECVE_CHECK on overlayfs, if the lower layer is
integrity-protected but the upper layer is writable, a copy-up between
the check and read() may cause the interpreter to read unverified
content."
Let us know what you think.
-Fan
^ 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