From: Gabriel Totev <[email protected]>

[ 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 <[email protected]>
Signed-off-by: John Johansen <[email protected]>
Signed-off-by: Sasha Levin <[email protected]>
---

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


Reply via email to