[PATCH bpf-next 03/29] bpf: introduce BPF token object
Andrii Nakryiko
andrii.nakryiko at gmail.com
Tue Jan 9 19:00:24 UTC 2024
On Tue, Jan 9, 2024 at 6:52 AM Christian Brauner <brauner at kernel.org> wrote:
>
> On Mon, Jan 08, 2024 at 03:58:47PM -0800, Andrii Nakryiko wrote:
> > On Mon, Jan 8, 2024 at 4:02 AM Christian Brauner <brauner at kernel.org> wrote:
> > >
> > > On Fri, Jan 05, 2024 at 02:18:40PM -0800, Andrii Nakryiko wrote:
> > > > On Fri, Jan 5, 2024 at 1:45 PM Linus Torvalds
> > > > <torvalds at linuxfoundation.org> wrote:
> > > > >
> > > > > Ok, I've gone through the whole series now, and I don't find anything
> > > > > objectionable.
> > > >
> > > > That's great, thanks for reviewing!
> > > >
> > > > >
> > > > > Which may only mean that I didn't notice something, of course, but at
> > > > > least there's nothing I'd consider obvious.
> > > > >
> > > > > I keep coming back to this 03/29 patch, because it's kind of the heart
> > > > > of it, and I have one more small nit, but it's also purely stylistic:
> > > > >
> > > > > On Wed, 3 Jan 2024 at 14:21, Andrii Nakryiko <andrii at kernel.org> wrote:
> > > > > >
> > > > > > +bool bpf_token_capable(const struct bpf_token *token, int cap)
> > > > > > +{
> > > > > > + /* BPF token allows ns_capable() level of capabilities, but only if
> > > > > > + * token's userns is *exactly* the same as current user's userns
> > > > > > + */
> > > > > > + if (token && current_user_ns() == token->userns) {
> > > > > > + if (ns_capable(token->userns, cap))
> > > > > > + return true;
> > > > > > + if (cap != CAP_SYS_ADMIN && ns_capable(token->userns, CAP_SYS_ADMIN))
> > > > > > + return true;
> > > > > > + }
> > > > > > + /* otherwise fallback to capable() checks */
> > > > > > + return capable(cap) || (cap != CAP_SYS_ADMIN && capable(CAP_SYS_ADMIN));
> > > > > > +}
> > > > >
> > > > > This *feels* like it should be written as
> > > > >
> > > > > bool bpf_token_capable(const struct bpf_token *token, int cap)
> > > > > {
> > > > > struct user_namespace *ns = &init_ns;
> > > > >
> > > > > /* BPF token allows ns_capable() level of capabilities, but only if
> > > > > * token's userns is *exactly* the same as current user's userns
> > > > > */
> > > > > if (token && current_user_ns() == token->userns)
> > > > > ns = token->userns;
> > > > > return ns_capable(ns, cap) ||
> > > > > (cap != CAP_SYS_ADMIN && capable(CAP_SYS_ADMIN));
> > > > > }
> > > > >
> > > > > And yes, I realize that the function will end up later growing a
> > > > >
> > > > > security_bpf_token_capable(token, cap)
> > > > >
> > > > > test inside that 'if (token ..)' statement, and this would change the
> > > > > order of that test so that the LSM hook would now be done before the
> > > > > capability checks are done, but that all still seems just more of an
> > > > > argument for the simplification.
> > > > >
> > > > > So the end result would be something like
> > > > >
> > > > > bool bpf_token_capable(const struct bpf_token *token, int cap)
> > > > > {
> > > > > struct user_namespace *ns = &init_ns;
> > > > >
> > > > > if (token && current_user_ns() == token->userns) {
> > > > > if (security_bpf_token_capable(token, cap) < 0)
> > > > > return false;
> > > > > ns = token->userns;
> > > > > }
> > > > > return ns_capable(ns, cap) ||
> > > > > (cap != CAP_SYS_ADMIN && capable(CAP_SYS_ADMIN));
> > > > > }
> > > >
> > > > Yep, it makes sense to use ns_capable with init_ns. I'll change those
> > > > two patches to end up with something like what you suggested here.
> > > >
> > > > >
> > > > > although I feel that with that LSM hook, maybe this all should return
> > > > > the error code (zero or negative), not a bool for success?
> > > > >
> > > > > Also, should "current_user_ns() != token->userns" perhaps be an error
> > > > > condition, rather than a "fall back to init_ns" condition?
> > > > >
> > > > > Again, none of this is a big deal. I do think you're dropping the LSM
> > > > > error code on the floor, and are duplicating the "ns_capable()" vs
> > > > > "capable()" logic as-is, but none of this is a deal breaker, just more
> > > > > of my commentary on the patch and about the logic here.
> > > > >
> > > > > And yeah, I don't exactly love how you say "ok, if there's a token and
> > > > > it doesn't match, I'll not use it" rather than "if the token namespace
> > > > > doesn't match, it's an error", but maybe there's some usability issue
> > > > > here?
> > > >
> > > > Yes, usability was the primary concern. The overall idea with BPF
> > >
> > > NAK on not restricting this to not erroring out on current_user_ns()
> > > != token->user_ns. I've said this multiple times before.
> >
> > I do restrict token usage to *exact* userns in which the token was
> > created. See bpf_token_capable()'s
> >
> > if (token && current_user_ns() == token->userns) { ... }
> >
> > and in bpf_token_allow_cmd():
> >
> > if (!token || current_user_ns() != token->userns)
> > return false;
> >
> > So I followed what you asked in [1] (just like I said I will in [2]),
> > unless I made some stupid mistake which I cannot even see.
> >
> >
> > What we are discussing here is a different question. It's the
> > difference between erroring out (that is, failing whatever BPF
> > operation was attempted with such token, i.e., program loading or map
> > creation) vs ignoring the token altogether and just using
> > init_ns-based capable() checks. And the latter is vastly more user
>
> Look at this:
>
> +bool bpf_token_capable(const struct bpf_token *token, int cap)
> +{
> + /* BPF token allows ns_capable() level of capabilities, but only if
> + * token's userns is *exactly* the same as current user's userns
> + */
> + if (token && current_user_ns() == token->userns) {
> + if (ns_capable(token->userns, cap))
> + return true;
> + if (cap != CAP_SYS_ADMIN && ns_capable(token->userns, CAP_SYS_ADMIN))
> + return true;
> + }
> + /* otherwise fallback to capable() checks */
> + return capable(cap) || (cap != CAP_SYS_ADMIN && capable(CAP_SYS_ADMIN));
> +}
>
> How on earth is it possible that the calling task is in a user namespace
> aka current_user_ns() == token->userns while at the same time being
> capable in the initial user namespace? When you enter an
> unprivileged user namespace you lose all capabilities against your
> ancestor user namespace and you can't reenter your ancestor user
> namespace.
>
> IOW, if current_user_ns() == token->userns and token->userns !=
> init_user_ns, then current_user_ns() != init_user_ns. And therefore that
> thing is essentially always false for all interesting cases, no?
>
Are you saying that this would be better?
if (token && current_user_ns() == token->userns) {
if (ns_capable(token->userns, cap))
return true;
if (cap != CAP_SYS_ADMIN && ns_capable(token->userns, CAP_SYS_ADMIN))
return true;
if (token->userns != &init_user_ns)
return false;
}
/* otherwise fallback to capable() checks */
return capable(cap) || (cap != CAP_SYS_ADMIN && capable(CAP_SYS_ADMIN));
I.e., return false directly if token's userns is not initns (there
will be also LSM check before this condition later on)? Falling back
to capable() checks and letting it return false if we are not in
init_ns or don't have capabilities seemed fine to me, that's all.
> Aside from that it would be semantically completely unclean. The user
> has specified a token and permission checking should be based on that
> token and not magically fallback to a capable check in the inital user
> namespace even if that worked.
I tried to explain the higher-level integration setup in [0]. The
thing is that users most of the time won't be explicitly passing a
token, BPF library will be passing it, if /sys/fs/bpf happens to be
mounted with delegation options.
So I wanted to avoid potential regressions (unintended and avoidable
failures) from using BPF token, because it might be hard to tell if a
BPF token is "beneficial" and is granting required permissions
(especially if you take into account LSM interactions). So I
consistently treat BPF token as optional/add-on permissions, not the
replacement for capable() checks.
It's true that it's unlikely that BPF token will be set up in init_ns
(except for testing, perhaps), but is it a reason to return -EPERM
without doing the same checks that would be done if BPF token wasn't
provided?
[0] https://lore.kernel.org/bpf/CAEf4Bzb6jnJL98SLPJB7Vjxo_O33W8HjJuAsyP3+6xigZtsTkA@mail.gmail.com/
>
> Because the only scenario where that is maybe useful is if an
> unprivileged container has dropped _both_ CAP_BPF and CAP_SYS_ADMIN from
> the user namespace of the container.
>
> First of, why? What thread model do you have then? Second, if you do
> stupid stuff like that then you don't get bpf in the container via bpf
> tokens. Period.
>
> Restrict the meaning and validity of a bpf token to the user namespace
> and do not include escape hatches such as this. Especially not in this
> initial version, please.
This decision fundamentally changes how BPF loader libraries like
libbpf will have to approach BPF token integration. It's not a small
thing and not something that will be easy to change later.
>
> I'm not trying to be difficult but it's clear that the implications of
> user namespaces aren't well understood here. And historicaly they are
I don't know why you are saying this. You haven't pointed out anything
that is actually broken in the existing implementation. Sure, you
might not be a fan of the approach, but is there anything
*technically* wrong with ignoring BPF token if it doesn't provide
necessary permissions for BPF operation and consistently using the
checks that would be performed with BPF token?
> exploit facilitators as much as exploit preventers.
More information about the Linux-security-module-archive
mailing list