Re: [PATCH] KEYS: prevent KEYCTL_READ on negative key

From: Eric Biggers
Date: Mon Sep 25 2017 - 14:35:50 EST


On Mon, Sep 25, 2017 at 02:29:56PM +0100, David Howells wrote:
> Eric Biggers <ebiggers3@xxxxxxxxx> wrote:
>
> > Putting the check in key_validate() would make lookups with
> > KEY_LOOKUP_PARTIAL stop returning negative keys, which would break
> > keyctl_describe(), keyctl_chown(), keyctl_setperm(), keyctl_set_timeout(),
> > keyctl_get_security() on negative keys. I presume those are supposed to
> > work?
>
> Lookups with KEY_LOOKUP_PARTIAL should never return a negative key by
> definition. A negative key is instantiated with an error code, so is no longer
> under construction.

Well, that's not what KEY_LOOKUP_PARTIAL actually does. KEY_LOOKUP_PARTIAL
allows the returned key to be uninstantiated, negatively instantiated, *or*
positively instantiated; and the callers seem to rely on that. Perhaps a better
name might have been KEY_LOOKUP_ALLOW_PARTIAL or KEY_LOOKUP_ALLOW_NONPOSITIVE.

On the other hand, without KEY_LOOKUP_PARTIAL, the returned key is required to
be positively instantiated. However, there is a special case. Namely, if no
permissions are requested, the returned key is allowed to be negative (as well
as revoked, invalidated, or expired --- though those can happen at any time
anyway until you do down_read(&key->sem)). I'm questioning whether we need that
special case.

>
> key_get_instantiation_authkey() must fail if the key has been constructed - but
> I guess there's a potential race in keyctl_describe_key(), keyctl_set_timeout()
> and keyctl_get_security() between getting the auth token and calling
> lookup_user_key() with perm of 0 in which the key could be instantiated,
> revoked, or instantiated elsewhere, or simply expire. This would allow the
> instantiating process a longer access window - but they do/did have a valid
> token...

Yes, by the time key_get_instantiation_authkey() returns, the key may have
already been instantiated. But I'm not too concerned about that, since the
caller must still have had a non-revoked authorization key shortly before.

>
> It should still be possible to describe, chown, setperm and get the security on
> negative keys by the normal access mechanism. Changing the timeout should
> probably be denied.
>
> > Another solution would be to remove the special case from lookup_user_key()
> > where it can return a negative/revoked/invalidated/expired key if
> > KEY_LOOKUP_PARTIAL is not specified and the 'perm' mask is 0.
>
> There are a number of circumstances in which it lookup_user_key() is called
> with perm==0, and in each case, the caller is responsible for handling the
> security:
>
> (1) keyctl_invalidate_key() will do so if the caller doesn't have permission,
> but CAP_SYS_ADMIN is set and the key is marked KEY_FLAG_ROOT_CAN_INVAL.
>
> (2) keyctl_keyring_clear() will do so if the caller doesn't have permission,
> but CAP_SYS_ADMIN is set and the key is marked KEY_FLAG_ROOT_CAN_CLEAR.
>
> (3) keyctl_keyring_unlink() will do so on the key-to-be-removed since only the
> keyring needs a perm check.
>
> (4) keyctl_read_key() always does so and then does the READ perm check and the
> possessor-can-SEARCH can search check itself.
>
> (5) keyctl_describe_key(), keyctl_set_timeout() and keyctl_get_security() will
> do so if the caller doesn't have permission, but does have a valid
> authorisation token. The latter requires that the key be under
> construction.

It's not about the permission checks. It's about whether a negative key is
allowed to be returned or not. And I think overloading 'perm' for that is not
really appropriate, and the cause of the bug in keyctl_read_key(). See the
code, it ignores the return value of wait_for_key_construction() if 'perm' is 0:

if (!(lflags & KEY_LOOKUP_PARTIAL)) {
ret = wait_for_key_construction(key, true);
switch (ret) {
case -ERESTARTSYS:
goto invalid_key;
default:
if (perm)
goto invalid_key;
case 0:
break;
}
}

>
> Functions that use KEY_LOOKUP_PARTIAL include:
>
> keyctl_describe_key()
> keyctl_chown_key()
> keyctl_setperm_key()
> keyctl_set_timeout()
> keyctl_get_security()
>
> all of which might need to be called from the upcall program. None of these
> should look at the payload.
>
> > The only callers it would affect are the case in question here which is
> > clearly a bug,
>
> keyctl_read_key() is definitely buggy. Actually, rather than manually testing
> KEY_FLAG_NEGATIVE there, it should probably use key_validate().

It already does use key_validate(). But key_validate() is also used in
lookup_user_key(), where it is expected to accept a negative key.

The real problem seems to be that the permissions mask rather than the flags
argument is used to tell lookup_user_key() whether it can return a negative key.

>
> > and the root-only exceptions for keyctl_invalidate() and
> > keyctl_clear(). And I suspect the latter two are unintentional as well.
>
> I'm not sure what you think is unintentional.
>

That root is allowed to invalidate or clear a
negative/invalidated/revoked/expired key or keyring but regular users cannot.

Again, the problem seems to be that the 'perm' argument is used for more than
just the permission check. I think the 'lflags' argument should indicate what
state the key is allowed to be in, not 'perm'.

> > (Is root *supposed* to be able to invalidate a
> > negative/revoked/invalidated/expired key, or clear a
> > revoked/invalidated/expired keyring?)
>
> You should be able to invalidate or unlink negative, revoked or expired keys if
> you have permission to do so. If you're using keyrings to cache stuff, you
> need to be able to invalidate negative results as well as positive ones.
>
> Invalidation of an invalidated key doesn't really make sense, but it shouldn't
> hurt. I can't immediately automatically remove all links to the invalidated
> key, but have to leave it to the garbage collector to effect.
>
> As for clearing of revoked/invalidated/expired keyrings, I'm not sure whether
> it makes sense to allow it - however, whilst keyrings are cleared upon
> revocation (since we have a definite point to do that with the key sem
> writelocked), they aren't automatically cleared upon expiry or invalidation, so
> it might make sense to permit it still.
>

Eric