Thread (24 messages) 24 messages, 8 authors, 2024-07-11

Re: [syzbot] [lsm?] general protection fault in hook_inode_free_security

From: Mickaël Salaün <mic@digikod.net>
Date: 2024-07-08 14:02:41
Also in: linux-fsdevel, lkml

On Thu, Jun 27, 2024 at 11:12:43AM -0700, Kees Cook wrote:
On Thu, Jun 27, 2024 at 03:34:41PM +0200, Mickaël Salaün wrote:
quoted
I didn't find specific issues with Landlock's code except the extra
check in hook_inode_free_security().  It looks like inode->i_security is
a dangling pointer, leading to UAF.

Reading security_inode_free() comments, two things looks weird to me:
quoted
/**
 * security_inode_free() - Free an inode's LSM blob
 * @inode: the inode
 *
 * Deallocate the inode security structure and set @inode->i_security to NULL.
I don't see where i_security is set to NULL.
Yeah, I don't either...
quoted
quoted
 */
void security_inode_free(struct inode *inode)
{
Shouldn't we add this check here?
if (!inode->i_security)
	return;
Probably, yes. The LSMs that check for NULL i_security in the free hook
all do so right at the beginning...
quoted
quoted
	call_void_hook(inode_free_security, inode);
	/*
	 * The inode may still be referenced in a path walk and
	 * a call to security_inode_permission() can be made
	 * after inode_free_security() is called. Ideally, the VFS
	 * wouldn't do this, but fixing that is a much harder
	 * job. For now, simply free the i_security via RCU, and
	 * leave the current inode->i_security pointer intact.
	 * The inode will be freed after the RCU grace period too.
It's not clear to me why this should be safe if an LSM try to use the
partially-freed blob after the hook calls and before the actual blob
free.
Yeah, it's not clear to me what the expected lifetime is here. How is
inode_permission() being called if all inode reference counts are 0? It
does seem intentional, though.

The RCU logic was introduced in commit 3dc91d4338d6 ("SELinux: Fix possible
NULL pointer dereference in selinux_inode_permission()"), with much
discussion:
https://lore.kernel.org/lkml/20140109101932.0508dec7@gandalf.local.home/ (local)
(This commit seems to remove setting "i_security = NULL", though, which
the comment implies is intended, but then it also seems to depend on
finding a NULL?)

LSMs using i_security are:

security/bpf/hooks.c:   .lbs_inode = sizeof(struct bpf_storage_blob),
security/integrity/evm/evm_main.c:      .lbs_inode = sizeof(struct evm_iint_cache),
security/integrity/ima/ima_main.c:      .lbs_inode = sizeof(struct ima_iint_cache *),
security/landlock/setup.c:      .lbs_inode = sizeof(struct landlock_inode_security),
security/selinux/hooks.c:       .lbs_inode = sizeof(struct inode_security_struct),
security/smack/smack_lsm.c:     .lbs_inode = sizeof(struct inode_smack),

SELinux is still checking for NULL. See selinux_inode() and
selinux_inode_free_security(), as do bpf_inode() and
bpf_inode_storage_free(). evm and ima also check for NULL.

landlock_inode() does not, though.

Smack doesn't hook the free, but it should still check for NULL, and it's not.

So I think this needs fixing in Landlock and Smack.

I kind of think that the LSM infrastructure needs to provide a common
helper for the "access the blob" action, as we've got it repeated in
each LSM, and we have 2 implementations that are missing NULL checks...
quoted
quoted
	 */
	if (inode->i_security)
		call_rcu((struct rcu_head *)inode->i_security,
			 inode_free_by_rcu);
And then:
inode->i_security = NULL;

But why call_rcu()?  i_security is not protected by RCU barriers.
I assume it's because security_inode_free() via __destroy_inode() via
destroy_inode() via evict() via iput_final() via iput() may be running
in interrupt context?

But I still don't see where i_security gets set to NULL. This won't fix
the permissions hook races for Landlock and Smack, but should make
lifetime a bit more clear?
It should not change anything.  I don't see how inode->i_security can be
NULL and when such an inode can be passed to an LSM hook.
quoted hunk ↗ jump to hunk
diff --git a/security/security.c b/security/security.c
index 9c3fb2f60e2a..a8658ebcaf0c 100644
--- a/security/security.c
+++ b/security/security.c
@@ -1613,7 +1613,8 @@ static void inode_free_by_rcu(struct rcu_head *head)
  */
 void security_inode_free(struct inode *inode)
 {
-	call_void_hook(inode_free_security, inode);
+	struct rcu_head *inode_blob = inode->i_security;
+
 	/*
 	 * The inode may still be referenced in a path walk and
 	 * a call to security_inode_permission() can be made
@@ -1623,9 +1624,11 @@ void security_inode_free(struct inode *inode)
 	 * leave the current inode->i_security pointer intact.
 	 * The inode will be freed after the RCU grace period too.
 	 */
-	if (inode->i_security)
-		call_rcu((struct rcu_head *)inode->i_security,
-			 inode_free_by_rcu);
+	if (inode_blob) {
+		call_void_hook(inode_free_security, inode);
+		inode->i_security = NULL;
If a path walk is ongoing, couldn't this lead to an LSM's security check
bypass?  Shouldn't we call all the inode_free_security() hooks in
inode_free_by_rcu()?  That would mean to reserve an rcu_head and then
probably use inode->i_rcu instead.

I think your patch is correct though.  Could you please send a full
patch?
+		call_rcu(inode_blob, inode_free_by_rcu);
+	}
 }
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help