Re: [RFC PATCH] lsm: fixup the inode xattr capability handling

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On May 4, 2024 1:04:57 PM Serge Hallyn <serge@xxxxxxxxxx> wrote:
May 2, 2024 19:59:11 Paul Moore <paul@xxxxxxxxxxxxxx>:

The current security_inode_setxattr() and security_inode_removexattr()
hooks rely on individual LSMs to either call into the associated
capability hooks (cap_inode_setxattr() or cap_inode_removexattr()), or
return a magic value of 1 to indicate that the LSM layer itself should
perform the capability checks.  Unfortunately, with the default return
value for these LSM hooks being 0, an individual LSM hook returning a
1 will cause the LSM hook processing to exit early, potentially
skipping a LSM.  Thankfully, with the exception of the BPF LSM, none
of the LSMs which currently register inode xattr hooks should end up
returning a value of 1, and in the BPF LSM case, with the BPF LSM hooks
executing last there should be no real harm in stopping processing of
the LSM hooks.  However, the reliance on the individual LSMs to either
call the capability hooks themselves, or signal the LSM with a return
value of 1, is fragile and relies on a specific set of LSMs being
enabled.  This patch is an effort to resolve, or minimize, these
issues.

Before we discuss the solution, there are a few observations and
considerations that we need to take into account:
* BPF LSM registers an implementation for every LSM hook, and that
 implementation simply returns the hook's default return value, a
 0 in this case.  We want to ensure that the default BPF LSM behavior
 results in the capability checks being called.
* SELinux and Smack do not expect the traditional capability checks
 to be applied to the xattrs that they "own".
* SELinux and Smack are currently written in such a way that the
 xattr capability checks happen before any additional LSM specific
 access control checks.  SELinux does apply SELinux specific access
 controls to all xattrs, even those not "owned" by SELinux.
* IMA and EVM also register xattr hooks but assume that the LSM layer
 and specific LSMs have already authorized the basic xattr operation.

In order to ensure we perform the capability based access controls
before the individual LSM access controls, perform only one capability
access control check for each operation, and clarify the logic around
applying the capability controls, we need a mechanism to determine if
any of the enabled LSMs "own" a particular xattr and want to take
responsibility for controlling access to that xattr.  The solution in
this patch is to create a new LSM hook, 'inode_xattr_skipcap', that is
not exported to the rest of the kernel via a security_XXX() function,
but is used by the LSM layer to determine if a LSM wants to control
access to a given xattr and avoid the traditional capability controls.
Registering an inode_xattr_skipcap hook is optional, if a LSM declines
to register an implementation, or uses an implementation that simply
returns the default value (0), there is no effect as the LSM continues
to enforce the capability based controls (unless another LSM takes
ownership of the xattr).  If none of the LSMs signal that the
capability checks should be skipped, the capability check is performed
and if access is granted the individual LSM xattr access control hooks
are executed, keeping with the DAC-before-LSM convention.

Signed-off-by: Paul Moore <paul@xxxxxxxxxxxxxx>
---
include/linux/lsm_hook_defs.h |  1 +
security/security.c           | 70 ++++++++++++++++++++++++-----------
security/selinux/hooks.c      | 28 ++++++++++----
security/smack/smack_lsm.c    | 31 +++++++++++++++-
4 files changed, 98 insertions(+), 32 deletions(-)

diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
index 334e00efbde4..6e54dae3256b 100644
--- a/include/linux/lsm_hook_defs.h
+++ b/include/linux/lsm_hook_defs.h
@@ -144,6 +144,7 @@ LSM_HOOK(int, 0, inode_setattr, struct mnt_idmap *idmap, struct dentry *dentry,
LSM_HOOK(void, LSM_RET_VOID, inode_post_setattr, struct mnt_idmap *idmap,
    struct dentry *dentry, int ia_valid)
LSM_HOOK(int, 0, inode_getattr, const struct path *path)
+LSM_HOOK(int, 0, inode_xattr_skipcap, const char *name)
LSM_HOOK(int, 0, inode_setxattr, struct mnt_idmap *idmap,
    struct dentry *dentry, const char *name, const void *value,
    size_t size, int flags)
diff --git a/security/security.c b/security/security.c
index 7e118858b545..1f5c68e2a62a 100644
--- a/security/security.c
+++ b/security/security.c
@@ -2278,7 +2278,20 @@ int security_inode_getattr(const struct path *path)
 * @size: size of xattr value
 * @flags: flags
 *
- * Check permission before setting the extended attributes.
+ * This hook performs the desired permission checks before setting the extended
+ * attributes (xattrs) on @dentry.  It is important to note that we have some
+ * additional logic before the main LSM implementation calls to detect if we
+ * need to perform an additional capability check at the LSM layer.
+ *
+ * Normally we enforce a capability check prior to executing the various LSM
+ * hook implementations, but if a LSM wants to avoid this capability check,
+ * it can register a 'inode_xattr_skipcap' hook and return a value of 1 for
+ * xattrs that it wants to avoid the capability check, leaving the LSM fully
+ * responsible for enforcing the access control for the specific xattr. If all
+ * of the enabled LSMs refrain from registering a 'inode_xattr_skipcap' hook,
+ * or return a 0 (the default return value), the capability check is still
+ * performed.  If no 'inode_xattr_skipcap' hooks are registered the capability
+ * check is performed.
 *
 * Return: Returns 0 if permission is granted.
 */
@@ -2286,20 +2299,20 @@ int security_inode_setxattr(struct mnt_idmap *idmap,
               struct dentry *dentry, const char *name,
               const void *value, size_t size, int flags)
{
-   int ret;
+   int rc;

   if (unlikely(IS_PRIVATE(d_backing_inode(dentry))))
       return 0;
-   /*
-    * SELinux and Smack integrate the cap call,
-    * so assume that all LSMs supplying this call do so.
-    */
-   ret = call_int_hook(inode_setxattr, idmap, dentry, name, value, size,
-               flags);

-   if (ret == 1)
-       ret = cap_inode_setxattr(dentry, name, value, size, flags);
-   return ret;
+   /* enforce the capability checks at the lsm layer, if needed */
+   if (!call_int_hook(inode_xattr_skipcap, name)) {
+       rc = cap_inode_setxattr(dentry, name, value, size, flags);
+       if (rc)
+           return rc;
+   }
+
+   return call_int_hook(inode_setxattr, idmap, dentry, name, value, size,
+                flags);
}

/**
@@ -2452,26 +2465,39 @@ int security_inode_listxattr(struct dentry *dentry)
 * @dentry: file
 * @name: xattr name
 *
- * Check permission before removing the extended attribute identified by @name
- * for @dentry.
+ * This hook performs the desired permission checks before setting the extended
+ * attributes (xattrs) on @dentry.  It is important to note that we have some
+ * additional logic before the main LSM implementation calls to detect if we
+ * need to perform an additional capability check at the LSM layer.
+ *
+ * Normally we enforce a capability check prior to executing the various LSM
+ * hook implementations, but if a LSM wants to avoid this capability check,
+ * it can register a 'inode_xattr_skipcap' hook and return a value of 1 for
+ * xattrs that it wants to avoid the capability check, leaving the LSM fully
+ * responsible for enforcing the access control for the specific xattr. If all
+ * of the enabled LSMs refrain from registering a 'inode_xattr_skipcap' hook,
+ * or return a 0 (the default return value), the capability check is still
+ * performed.  If no 'inode_xattr_skipcap' hooks are registered the capability
+ * check is performed.
 *
 * Return: Returns 0 if permission is granted.
 */
int security_inode_removexattr(struct mnt_idmap *idmap,
                  struct dentry *dentry, const char *name)
{
-   int ret;
+   int rc;

   if (unlikely(IS_PRIVATE(d_backing_inode(dentry))))
       return 0;
-   /*
-    * SELinux and Smack integrate the cap call,
-    * so assume that all LSMs supplying this call do so.
-    */
-   ret = call_int_hook(inode_removexattr, idmap, dentry, name);
-   if (ret == 1)
-       ret = cap_inode_removexattr(idmap, dentry, name);
-   return ret;
+
+   /* enforce the capability checks at the lsm layer, if needed */
+   if (!call_int_hook(inode_xattr_skipcap, name)) {

Hm, so if it should happen that lsm 2 returns 0 (allow) but lsm 3
has skipcap return 3, and lsm 3 would have returned
1 to deny the remove, we will get an unexpected result.  It feels like
we need a stronger tie between the lsm which allowed and the one
saying skip the capability check.

That's not an unexpected result, that is a valid outcome in the world of LSM stacking. The skipcap check only guarantees that the capability check will be skipped if an LSM returns a non-zero value. The vast majority (all?) of the hooks operate as you describe: a LSM towards the back of the list can reject an operation that was previous LSM has allowed. This isn't limited to LSMs either, there are plenty of reasons, e.g. transient failures, which could cause an operation to fail after being authorized by a particular LSM.

A particular LSM can only authorize a requested operation; a successful return value from a LSM hook implementation can not guarantee a successful operation result.

--
paul-moore.com








[Index of Archives]     [Selinux Refpolicy]     [Linux SGX]     [Fedora Users]     [Fedora Desktop]     [Yosemite Photos]     [Yosemite Camping]     [Yosemite Campsites]     [KDE Users]     [Gnome Users]

  Powered by Linux