[apparmor] [PATCH AUTOSEL 6.16-6.6] apparmor: shift ouid when mediating hard links in userns

Sasha Levin sashal at kernel.org
Fri Aug 8 15:30:41 UTC 2025


From: Gabriel Totev <gabriel.totev at zetier.com>

[ Upstream commit c5bf96d20fd787e4909b755de4705d52f3458836 ]

When using AppArmor profiles inside an unprivileged container,
the link operation observes an unshifted ouid.
(tested with LXD and Incus)

For example, root inside container and uid 1000000 outside, with
`owner /root/link l,` profile entry for ln:

/root$ touch chain && ln chain link
==> dmesg
apparmor="DENIED" operation="link" class="file"
namespace="root//lxd-feet_<var-snap-lxd-common-lxd>" profile="linkit"
name="/root/link" pid=1655 comm="ln" requested_mask="l" denied_mask="l"
fsuid=1000000 ouid=0 [<== should be 1000000] target="/root/chain"

Fix by mapping inode uid of old_dentry in aa_path_link() rather than
using it directly, similarly to how it's mapped in __file_path_perm()
later in the file.

Signed-off-by: Gabriel Totev <gabriel.totev at zetier.com>
Signed-off-by: John Johansen <john.johansen at canonical.com>
Signed-off-by: Sasha Levin <sashal at kernel.org>
---

LLM Generated explanations, may be completely bogus:

**Backport Status: YES**

This commit should be backported to stable kernel trees for the
following reasons:

1. **Clear Bug Fix**: This fixes a real bug where AppArmor incorrectly
   reports the unshifted uid when mediating hard link operations inside
   user namespaces/containers. The commit message provides a concrete
   example showing ouid=0 instead of the expected ouid=1000000 in
   container logs.

2. **Security Impact**: This is a security-relevant bug that causes
   AppArmor policy enforcement to behave incorrectly in containerized
   environments. The owner-based AppArmor rules (like `owner /root/link
   l,`) won't work correctly because the uid comparison is done with the
   wrong (unshifted) value.

3. **Minimal and Contained Fix**: The change is small and surgical - it
   only modifies the aa_path_link() function to properly map the inode
   uid through the mount's idmapping, following the exact same pattern
   already used in __file_path_perm():
   - Uses `i_uid_into_vfsuid(mnt_idmap(target.mnt), inode)` to get the
     vfsuid
   - Converts it back with `vfsuid_into_kuid(vfsuid)` for the path_cond
     structure

4. **No New Features or Architecture Changes**: This is purely a bug fix
   that makes aa_path_link() consistent with how __file_path_perm()
   already handles uid mapping. No new functionality is added.

5. **Container/User Namespace Compatibility**: With the widespread use
   of containers (LXD, Incus, Docker with userns), this bug affects many
   production systems. The fix ensures AppArmor policies work correctly
   in these environments.

6. **Low Risk**: The change follows an established pattern in the
   codebase (from __file_path_perm) and only affects the specific case
   of hard link permission checks in user namespaces. The risk of
   regression is minimal.

7. **Clear Testing**: The commit message indicates this was tested with
   LXD and Incus containers, showing the issue is reproducible and the
   fix validated.

The code change is straightforward - replacing direct access to
`d_backing_inode(old_dentry)->i_uid` with proper idmapping-aware
conversion that respects user namespace uid shifts. This makes
aa_path_link() consistent with other AppArmor file operations that
already handle idmapped mounts correctly.

 security/apparmor/file.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/security/apparmor/file.c b/security/apparmor/file.c
index d52a5b14dad4..62bc46e03758 100644
--- a/security/apparmor/file.c
+++ b/security/apparmor/file.c
@@ -423,9 +423,11 @@ int aa_path_link(const struct cred *subj_cred,
 {
 	struct path link = { .mnt = new_dir->mnt, .dentry = new_dentry };
 	struct path target = { .mnt = new_dir->mnt, .dentry = old_dentry };
+	struct inode *inode = d_backing_inode(old_dentry);
+	vfsuid_t vfsuid = i_uid_into_vfsuid(mnt_idmap(target.mnt), inode);
 	struct path_cond cond = {
-		d_backing_inode(old_dentry)->i_uid,
-		d_backing_inode(old_dentry)->i_mode
+		.uid = vfsuid_into_kuid(vfsuid),
+		.mode = inode->i_mode,
 	};
 	char *buffer = NULL, *buffer2 = NULL;
 	struct aa_profile *profile;
-- 
2.39.5




More information about the AppArmor mailing list