Re: [PATCH bpf-next 2/2] selftests/bpf: Add tests for bpf_get_dentry_xattr

From: Song Liu
Date: Mon Aug 19 2024 - 16:26:00 EST


Hi Christian,

> On Aug 19, 2024, at 4:16 AM, Christian Brauner <brauner@xxxxxxxxxx> wrote:

[...]

>> Did I get this right?
>>
>> IIUC, there are a few issues with this approach.
>>
>> 1. security_inode_permission takes inode as parameter. However, we need
>> dentry to get the xattr. Shall we change security_inode_permission
>> to take dentry instead?
>> PS: Maybe we should change most/all inode hooks to take dentry instead?
>
> security_inode_permission() is called in generic_permission() which in
> turn is called from inode_permission() which in turn is called from
> inode->i_op->permission() for various filesystems. So to make
> security_inode_permission() take a dentry argument one would need to
> change all inode->i_op->permission() to take a dentry argument for all
> filesystems. NAK on that.
>
> That's ignoring that it's just plain wrong to pass a dentry to
> **inode**_permission() or security_**inode**_permission(). It's
> permissions on the inode, not the dentry.

Agreed.

[...]

>>>
>>> Which means this code ends up charging relative lookups twice. Even if one
>>> irons that out in the program this encourages really bad patterns.
>>> Path lookup is iterative top down. One can't just randomly walk back up and
>>> assume that's equivalent.
>>
>> I understand that walk-up is not equivalent to walk down. But it is probably
>> accurate enough for some security policies. For example, LSM LandLock uses
>> similar logic in the file_open hook (file security/landlock/fs.c, function
>> is_access_to_paths_allowed).
>
> I'm not well-versed in landlock so I'll let Mickaël comment on this with
> more details but there's very important restrictions and differences
> here.
>
> Landlock expresses security policies with file hierarchies and
> security_inode_permission() doesn't and cannot have access to that.
>
> Landlock is subject to the same problem that the BPF is here. Namely
> that the VFS permission checking could have been done on a path walk
> completely different from the path walk that is checked when walking
> back up from security_file_open().
>
> But because landlock works with a deny-by-default security policy this
> is ok and it takes overmounts into account etc.
>
>>
>> To summary my thoughts here. I think we need:
>>
>> 1. Change security_inode_permission to take dentry instead of inode.
>
> Sorry, no.
>
>> 2. Still add bpf_dget_parent. We will use it with security_inode_permission
>> so that we can propagate flags from parents to children. We will need
>> a bpf_dput as well.
>> 3. There are pros and cons with different approaches to implement this
>> policy (tags on directory work for all files in it). We probably need
>> the policy writer to decide with one to use. From BPF's POV, dget_parent
>> is "safe", because it won't crash the system. It may encourage some bad
>> patterns, but it appears to be required in some use cases.
>
> You cannot just walk a path upwards and check permissions and assume
> that this is safe unless you have a clear idea what makes it safe in
> this scenario. Landlock has afaict. But so far you only have a vague
> sketch of checking permissions walking upwards and retrieving xattrs
> without any notion of the problems involved.

I am sorry for being vague with the use case here. We are trying to
cover a few different use cases, such as sandboxing, allowlisting
certain operations to selected binaries, prevent operation errors, etc.
For this work, we are looking for the right building blocks to enable
these use cases.

> If you provide a bpf_get_parent() api for userspace to consume you'll
> end up providing them with an api that is extremly easy to misuse.

Does this make sense to have higher level API that walks up the path,
so that it takes mounts into account. It can probably be something like:

int bpf_get_parent_path(struct path *p) {
again:
if (p->dentry == p->mnt.mnt_root) {
follow_up(p);
goto again;
}
if (unlikely(IS_ROOT(p->dentry))) {
return PARENT_WALK_DONE;
}
parent_dentry = dget_parent(p->dentry);
dput(p->dentry);
p->dentry = parent_dentry;
return PARENT_WALK_NEXT;
}

This will handle the mount. However, we cannot guarantee deny-by-default
policies like LandLock does, because this is just a building block of
some security policies.

Is this something we can give a try with?

Thanks,
Song