ACK/Cmnt: [PATCH 2/2] drm/i915: Synchronize active and retire callbacks
Andrea Righi
andrea.righi at canonical.com
Fri Apr 10 08:53:08 UTC 2020
On Thu, Apr 09, 2020 at 09:59:11AM -0700, Sultan Alsawaf wrote:
> On Thu, Apr 09, 2020 at 06:40:43PM +0200, Andrea Righi wrote:
> > > /* After the final retire, the entire struct may be freed */
> > > - if (ref->retire)
> > > - ref->retire(ref);
> > > + if (ref->retire) {
> > > + if (ref->active) {
> > > + bool freed = false;
> > > +
> > > + /* Don't race with the active callback, and avoid UaF */
> > > + down_write(&ref->rwsem);
> > > + ref->freed = &freed;
> >
> > So, we allocate "freed" in the stack and assign to ref->freed, so
> > another thread can potentially access this thread's stack... hm.. I'm
> > not sure if this is a safe practice.
>
> ref->retire() isn't asynchronous for any of the functions registered to it, so
> no other thread accesses this thread's stack.
>
> > I'm still trying to understand the logic behind this, sorry if I'm
> > asking something dumb... but why not doing something simple like this:
> >
> > down_write(&ref->rwsem);
> > if (ref->retire)
> > ref->retire(ref);
> > up_write(&ref->rwsem);
>
> I actually made this mistake with one of my earlier attempts. `ref->retire()`
> will free `ref` on the final put, so putting `up_write(&ref->rwsem);` after it
> unconditionally is a use-after-free. And we have no way to access the reference
> counter that determines when the final put occurs, because it varies by each
> user of the API.
>
> Plus, all the functions involved in the retirement sequence do not return
> values, so trying to make `ref->retire()` return a boolean that says whether
> `ref` was freed or not would require extensive changes across multiple files.
>
> > Again, why not a simple:
> >
> > down_read(&ref->rwsem);
> > if (!atomic_read(&ref->count) && ref->active)
> > err = ref->active(ref);
> > up_read(&ref->rwsem);
>
> No reason to hold a lock when we don't need to. There are some users of this API
> that do not have a retire callback (i.e., `ref->retire` is NULL), so in that
> case we don't need to do any synchronization.
>
> > > + bool *freed;
> >
> > Why does freed need to be a pointer? Why not just a bool?
>
> Because everything inside the `ref` struct can be freed after `ref->retire()`
> executes, so accessing `ref` after that would be a use-after-free.
>
> Thanks for the review!
>
> Sultan
After looking many times at the code and your comments this is what I
understand so far:
- ref->retire() and ref->active() can run concurrently when they are
both defined and we want to prevent this
- ref->retire() can destroy ref itself when ref->count becomes 0
- ref->active() can call itself in a recursive way
- i915_active_fini() is called immediately before ref is destroyed
- there are no multiple ref->retire() running at the same time for the
same ref object (same with ref->active())
The code where we start is very difficult to read (not your fault
obviously) and to make it look nicer I think we would need a complete
redesign, that is not a viable option at the moment.
Assuming all of the above is correct I think your solution is
acceptable, therefore:
Acked-by: Andrea Righi <andrea.righi at canonical.com>
---
Just for fun (if it helps) this is how I would do it: less efficient,
but maybe a little more readable (even if the code is still ugly).
I think the __acquires/__releases annotations are helping a bit to
follow the logic, but in particular I would really like to get rid of
that ref->freed pointer used to access data allocated in current's
stack, that makes me feel a little nervous.
struct i915_active {
...
struct rb_root tree;
struct mutex mutex;
atomic_t count;
struct rw_semaphore rwsem;
bool freed;
...
}
void __i915_active_init(struct i915_active *ref,
int (*active)(struct i915_active *ref),
void (*retire)(struct i915_active *ref),
struct lock_class_key *mkey,
struct lock_class_key *wkey)
{
...
ref->freed = false;
__init_rwsem(&ref->rwsem, "i915_active.rwsem", rkey);
...
}
static void
__active_retire(struct i915_active *ref) __acquires(&ref->rwsem)
{
...
if (ref->retire) {
bool freed = false;
/* Don't race with the active callback, and avoid UaF */
down_write(&ref->rwsem);
if (atomic_read(&ref->count) == 1) {
/*
* The next call to ref->retire() will free
* "ref" on the last put, ref->rwsem will be
* released in i915_active_fini(), called
* immediately before freeing "ref"
*/
ref->freed = true;
freed = true;
}
ref->retire(ref);
if (!freed)
up_write(&ref->rwsem);
}
...
}
int i915_active_acquire(struct i915_active *ref)
{
...
if (!atomic_read(&ref->count) && ref->active) {
down_read(&ref->rwsem);
err = ref->active(ref);
up_read(&ref->rwsem);
}
...
}
void i915_active_fini(struct i915_active *ref) __releases(&ref->rwsem)
{
if (ref->freed)
up_write(&ref->rwsem);
#if IS_ENABLED(CONFIG_DRM_I915_DEBUG_GEM)
debug_active_fini(ref);
GEM_BUG_ON(!RB_EMPTY_ROOT(&ref->tree));
GEM_BUG_ON(atomic_read(&ref->count));
mutex_destroy(&ref->mutex);
#endif
}
Thanks!
-Andrea
More information about the kernel-team
mailing list