scoped_refptr has 2 ways to count references |
|||
Issue description
Avi notes that `scoped_refptr` can count references both from 0 and from 1:
```
enum StartRefCountFromZeroTag { kStartRefCountFromZeroTag };
enum StartRefCountFromOneTag { kStartRefCountFromOneTag };
```
Reference counting is too easy to get wrong and to important to get wrong, so weird complexity is a bug.
I'm filing this as a Type=Task for now, but things like this can become Type=Bug-Security (see e.g. https://bugs.chromium.org/p/project-zero/issues/list?can=1&q=reference+count+overflow&colspec=ID+Status+Restrict+Reported+Vendor+Product+Finder+Summary&cells=ids). Let's try to stop that from happening. :)
,
Jan 7
The zero-based reference count can also result in UAFs due to the object being in this weird pending state. For example:
// Correct independent of how Foo::Start works.
scoped_refptr<Foo> MakeFooAndStart() {
scoped_refptr<Foo> foo = base::MakeRefCounted<Foo>();
foo->Start();
return foo;
}
void UseFoo() {
scoped_refptr<Foo> foo = MakeFooAndStart();
DoSomething(foo.get());
}
// Incorrect, but often shows up in older code. This usually works but sometimes fails catastrophically.
Foo* MakeFooAndStart() {
Foo* foo = new Foo;
foo->Start();
return foo;
}
void UseFoo() {
scoped_refptr<Foo> foo = MakeFooAndStart();
DoSomething(foo.get());
}
The problem is that MakeFooAndStart's bare Foo* is in this weird zero-ref state. It has an illegal reference count and is waiting for its owner to adopt it. This is the only time where AddRef() + Release() is not a no-op.
So that means that, depending on how Start is implemented internally (does it take a reference and then release it?), foo may be destroyed by the time we return it to UseFoo. This can even be a race condition. Consider if this code ran on thread A, but Foo::Start posted a task to thread B. If that task completes fast enough, we may have destroyed Foo.
One path is to start requiring base::MakeRefCounted for everything, but then we run into the same private constructor problem as std::make_unique. Skimming scoped_refptr.h, I see base::AdoptRef, but that seems to only work if the type has already opted into 1-based-ref-counting. Should it maybe work for both so we can write tooling for this?
Or maybe we should exclusively use base::MakeRefCounted<T> and use the passkey idiom (https://abseil.io/tips/134) for private constructors? I like the definitiveness of "no bare new, only use make_unique / MakeRefCounted".
,
Jan 7
I'd like to get rid of "start from zero" refcounting entirely, for simplicity's sake if nothing else. The effort to migrate all types is a long-due refactoring work.
,
Jan 7
FYI, a while back I tried to rid base/bind_internal.h of this by replacing "new BindState" by "base::MakeRefCounted<BindState>" in bind_internal.h (moving the single ref around to ultimately avoid the AdoptRef() in callback_internal.h) but it regressed binary size by 475KB and I had to abandon :( https://chromium-review.googlesource.com/c/chromium/src/+/864044/3#message-e604e29b1ca730faf533c08a1ebf3018f4760882
,
Jan 7
,
Jan 7
Perhaps the compiler's not smart enough to figure out that, when you move out of a scoped_refptr, the destructor is a no-op? dcheng mentioned to me once that the post-C++11 pass-by-value rules have some binary size consequences. |
|||
►
Sign in to add a comment |
|||
Comment 1 by kylec...@chromium.org
, Jan 7