New issue
Advanced search Search tips

Issue 919580 link

Starred by 4 users

Issue metadata

Status: Available
Owner: ----
Cc:
Components:
EstimatedDays: ----
NextAction: ----
OS: Linux , Android , Windows , iOS , Chrome , Mac , Fuchsia
Pri: 3
Type: Task



Sign in to add a comment

scoped_refptr has 2 ways to count references

Project Member Reported by palmer@chromium.org, Jan 7

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. :)
 
Cc: kylec...@chromium.org
Cc: davidben@chromium.org
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".
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.
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
Cc: gab@chromium.org
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