调试chromium cookie 引发的崩溃

最近做M55内核升级,我负责浏览器的稳定性。陆陆续续把各种崩溃都修复的差不多了,只剩下最后一个崩溃了,其崩溃堆栈如下:

0599f4a0 534f0c75 00000000 0f774418 00000000 chrome!net::URLRequestHttpJob::SaveCookiesAndNotifyHeadersComplete+0x2e4
0599f534 537e5094 00000000 0599f638 534ba4fd chrome!net::URLRequestHttpJob::OnStartCompleted+0x216
0599f540 534ba4fd 0ef65fb0 0599f614 0599f618 chrome!base::internal::Invoker<base::internal::BindState<void (__thiscall update_client::TaskUpdate::*)(int),base::internal::UnretainedWrapper<update_client::TaskUpdate> >,void __cdecl(int)>::Run+0x11
0599f638 53c6f3ce 00000000 00000000 0599f65c chrome!net::HttpCache::Transaction::DoLoop+0x2a4
0599f648 52e3aa46 07df4da0 07df4da4 0599f67c chrome!base::internal::InvokeHelper<1,void>::MakeItSo<void (__thiscall data_reduction_proxy::DBDataOwner::*const &)(data_reduction_proxy::DataUsageBucket *),base::WeakPtr<data_reduction_proxy::DBDataOwner> const &,data_reduction_proxy::DataUsageBucket *>+0x26
0599f65c 534fc7d6 07df4d90 0599f67c 080ad2a4 chrome!base::internal::Invoker<base::internal::BindState<void (__thiscall DownloadTargetDeterminer::*)(enum content::DownloadDangerType),base::WeakPtr<DownloadTargetDeterminer> >,void __cdecl(enum content::DownloadDangerType)>::Run+0x16
0599f6a4 534fb2ba 0f7e8990 00000000 54a75558 chrome!disk_cache::InFlightBackendIO::OnOperationComplete+0xfd
0599f6c0 534fb420 0f7e8990 00000000 0599f984 chrome!disk_cache::InFlightIO::InvokeCallback+0x57
0599f724 530ff66b 0be810b0 0599f76c 01161ad0 chrome!disk_cache::BackgroundIO::OnIOSignalled+0xb7
0599f784 530b23b9 544e2dec 54a75553 01161ad0 chrome!base::debug::TaskAnnotator::RunTask+0x10b
0599f8c8 530b1c81 0599f984 01175da8 00000000 chrome!base::MessageLoop::RunTask+0x2e9
0599f9d0 5310048a 00000000 01175da8 5441f998 chrome!base::MessageLoop::DoWork+0x2a1
0599fab4 531015ba 5433b4a4 0599fcc4 01161ad0 chrome!base::MessagePumpForIO::DoRunLoop+0xba
0599fadc 530b20cb 01161ad0 530e3625 01121818 chrome!base::MessagePumpWin::Run+0x4a
0599fae4 530e3625 01121818 022be07b 00000000 chrome!base::MessageLoop::RunHandler+0xb
0599fb08 530cf133 00000000 0599fbe0 529e9534 chrome!base::RunLoop::Run+0x65
0599fb14 529e9534 0599fcc4 53f4409a 52df358a chrome!base::Thread::Run+0x13
0599fbe0 529e9ab6 0599fcc4 00000000 01121849 chrome!content::BrowserThreadImpl::IOThreadRun+0x1e
0599fcac 530cf56c 0599fcc4 77041420 01145638 chrome!content::BrowserThreadImpl::Run+0xf6
0599fcfc 5309c072 00000000 00000000 01145638 chrome!base::Thread::ThreadMain+0x12c
0599fd18 7704336a 0000033c 0599fd64 77aa9902 chrome!base::`anonymous namespace'::ThreadFunc+0x82
0599fd24 77aa9902 01145638 70053da7 00000000 kernel32!BaseThreadInitThunk+0xe
0599fd64 77aa98d5 5309bff0 01145638 00000000 ntdll!__RtlUserThreadStart+0x70
0599fd7c 00000000 5309bff0 01145638 00000000 ntdll!_RtlUserThreadStart+0x1b

在执行这行代码的时候发生了崩溃:

request_->context()->cookie_store()->SetCookieWithOptionsAsync(request_->url(), cookie, options, CookieStore::SetCookiesCallback());

初步一看,这个崩溃点在Chromium原生的代码里面,所以怀疑是Chromium自己的bug。于是在https://bugs.chromium.org/p/chromium/issues/list里面搜索相关堆栈关键词,看看能不能找到类似的崩溃。很遗憾的是并没有搜到有用的信息。

然后再去https://groups.google.com/a/chromium.org/forum/#!forum/chromium-dev里面碰碰运气,仍然是一无所获。看来是指望不上Chromium社区的帮助了,只能自己动手,丰衣足食。

因为我们抓的崩溃dump只包含栈上的内存,另外浏览器Release版本做了编译器优化,导致很多栈上变量值都优化的不可见。因此崩溃dump里面包含的有用信息并不多,只能看到浏览器崩溃在哪行代码,却不能观察到浏览器崩溃时所处的状态,如Windbg里面查看到变量信息下图所示: windbg locals

为此,通过Alias函数引用到栈上变量,防止它被优化,Alias函数如下图所示:

#pragma optimize("", off)
void Alias(const void* var) {
}
#pragma optimize("", on)

然后把自己关心的信息,都存到栈上分配的内存上,比如我想知道崩溃的时候请求的url、cookie等信息,如下图所示:

      const URLRequestContext* url_request_context = request_->context();
      base::debug::Alias(&url_request_context);

      CookieStore* cookie_store = request_->context()->cookie_store();
      base::debug::Alias(&cookie_store);

      char cookie_buffer[1024] = {0};
      base::strlcpy(cookie_buffer, cookie.c_str(), arraysize(cookie_buffer));
      base::debug::Alias(cookie_buffer);

      char url_buffer[1024] = {0};
      base::strlcpy(url_buffer, request_->url().spec().c_str(),
                    arraysize(url_buffer));
      base::debug::Alias(url_buffer);

      char first_party_for_cookies_buffer[1024] = {0};
      base::strlcpy(first_party_for_cookies_buffer,
                    request_->first_party_for_cookies().spec().c_str(),
                    arraysize(first_party_for_cookies_buffer));
      base::debug::Alias(first_party_for_cookies_buffer);

      char referrer_buffer[1024] = {0};
      base::strlcpy(referrer_buffer, request_->referrer().c_str(),
                    arraysize(referrer_buffer));
      base::debug::Alias(referrer_buffer);

加上这些收集调试信息的代码,再灰度出去收集新的dump。通过分析新的dump果然有了一些有用的信息。可以看到浏览器崩溃的时候first_party_for_cookies对于的url都是天猫、淘宝的商品页面,而此时网络请求的url也都是https://gm.mmstat.com/tbdetail的url,如下图所示: windbg locals

可以确定浏览器都是访问淘宝天猫出现的崩溃,但是我本地试了试,并不能复现问题。为了找到可以复现的崩溃场景,我在浏览器崩溃的时候,利用QQ的tencent://协议自动打开QQ跟我联系,代码如下: windbg locals

继续灰度出去一版,等待崩溃的用户联系我。

我这边继续分析之前的崩溃dump,可以排除是空指针崩溃。发现cookie_store变量很有意思,其中一个dump如下: windbg locals

可以看到cookie_store指向的net::CookieStore对象的虚函数表地址竟然是0xe43a00f2,已经跑到系统的地址空间里面去了,显然超出了合法的地址范围。而有的dump则是这样: windbg locals

虽然虚函数表地址是正常的,但是其中虚函数指针都是错的,我猜可能是UAF(Use After Free)错误。就是request_->context()->cookie_store()指针指向的对象已经被销毁了,指针已经成了悬挂指针,再运行这行代码会有不可预知的错误。

崩溃用户跟我联系的那边情况是:崩溃的用户多是淘宝的卖家,他们说浏览器用着用着,忽然就自己崩溃了,没有什么必现崩溃规律。我看了下dump里面进程运行的时间,有的进程运行了几分钟,有的运行了好几个小时。这是个不定时的崩溃,没有什么复现规律,真是难办啊。

我继续从代码层面分析,看看什么地方的逻辑把CookieStore给销毁了。可以看到CookieStore对象是在ProfileImplIOData里面创建并管理其生命周期的,ProfileImplIOData的生命周期几乎跟浏览器一样长,也就意味着CookieStore的生命周期几乎跟浏览器一样长。不太可能出现CookieStore销毁了而URLRequestContext继续持有一个悬挂的指针。

想到都是崩溃在天猫淘宝页面,而用户多是淘宝卖家,会不会是我们的卖家窗口出了问题呢?浏览器的卖家窗口会创建一个ProfilePartitionIOData的对象,跟ProfileImplIOData对象类似。对比从M50内核升级到M55内核的代码,发现其管理CookieStore生命周期的逻辑变了。

这是M50内核的代码:

  main_request_context_->set_cookie_store(
      content::CreateCookieStore(cookie_config));

可以看到通过CreateCookieStore创建的对象传给了URLRequestContext,后面并没有销毁对象的逻辑。而M55内核的这块的代码的如下:

main_cookie_store_ = content::CreateCookieStore(cookie_config);
  main_request_context_->set_cookie_store(main_cookie_store_.get());

main_cookie_store_ 是个std::unique_ptr成员变量,CookieStore在ProfilePartitionIOData销毁的时候销毁。而卖家窗口在浏览器的生命周期中可能会多次创建和销毁。因此是可能存在CookieStore被销毁而后续的URLRequestContext继续持有并使用它的指针的情况。

然后自己写代码模拟这种情况,果然如线上用户崩溃的情形类似。在https://cs.chromium.org/上查看profile_impl_io_data.cc的log,最新的Chromim页面也把这块逻辑又改回了: windbg locals

查看相关的log信息:

Move ownership of objects from ProfileImplIOData to ProfileIOData.

Adds a URLRequestContextStorage object to ProfileIOData to hold objects
created by its subclasses.  Plan to move some objects ProfileIOData owns
itself to this object as well to simplify things a bit, and share more
initialization code between subclasses.

This should prevent UAF during shutdown when there's a live request
owned by a ProfileIOData object, in some cases.

BUG=651993

Review-Url: https://codereview.chromium.org/2393223002
Cr-Commit-Position: refs/heads/master@{#425738}