在没有ASLR的情况下猎杀一只消失的(记忆,GC相关)黑森博格



OS:Linux/Debian/Sid/x86_64(和Linux/Debian/Testing/x86_66);我用于编译的系统GCC是6.1.1(Debian/Testing是5.3)。Gnu-libc为2.22;Linux内核为4.5;GDB是7.10系统或我自己的系统,由FSF源代码7.11构建

我正在寻找(从近两周)一个记忆&垃圾收集GCC的MELT实验分支中的相关heisenbug

svn co -r236207 svn://gcc.gnu.org/svn/gcc/branches/melt-branch gcc-melt

然后(对于每个GCC变体或分支)将其构建在外部的树中,例如

mkdir _ObjMelt
cd _ObjMelt
../gcc-melt/configure  --disable-bootstrap --enable-checks=gc 
 --enable-plugins --disable-multilib --enable-languages=c,c++,lto

(您可以将其他选项传递给../gcc-melt/configure,例如CXXFLAGS='-g3 -O0 -DMELT_HAVE_RUNTIME_DEBUG=1',如果您愿意;您可以删除--enable-checks=gc选项)

当然还有make(或make -j4);该构建可能需要半个多小时(ASLR可能会失败,见下文)

MELT有一个世代复制垃圾收集器(我怀疑这个bug是其中的一个角落),并使用了大量元编程(特别是,复制GC的大多数扫描和转发代码都是由MELT生成的)。

valgrind在这里没有帮助:我们正在实现复制GC,而GCC本身——即使没有MELT——也会泄漏内存)

MELT是自举的。通常的构建过程是从MELT源代码中重新生成两次发出的C++代码。通常的方法是发出一些C++代码,派生一些make来获得共享对象,dlopen来获得该共享对象,然后再次。

如果没有ASLR,构建总是成功的(它正在运行一个重要的测试:MELT的引导,以及通过MELT扩展的编译对MELT运行时的分析)。我甚至可以用make upgrade-warmelt重新生成运行时代码。

但是启用ASLR时,构建失败,崩溃总是以相同的方式(注意cc1plus是MELT):

cc1plus: note: MELT got fatal failure from ../../gcc-melt/gcc/melt-runtime.h:900
cc1plus: fatal error: corrupted memory heap with null magic discriminant
                      in 0x2bab6a8; GC#11
compilation terminated.
MELT BUILD SCRIPT FAILURE: 
  melt-build-script.tpl:382/307-melt-build-script.tpl:459/382 failed 
  with arguments @meltbuild-stage2/warmelt-normatch.args

我正在禁用ASLR,例如使用exec setarch $(uname -m) -R /bin/bash;当然,当运行uder gdb时,默认情况下会禁用ASLR(除非我将set disable-randomization 0作为GDB命令执行)。

我的同事Franck Védrine建议我使用gdb的反向执行功能;原则上,它应该像在我的GC中设置断点一样简单(以及在melt_fatal_error宏调用的fatal_error&melt_fatal_info中…),达到GC#11状态,做一个record用于后续的向后执行,运行故障案例(使用set disable-randomization 0禁用ASLR)直到"崩溃",然后reverse-cont直到GC中的断点,并明智地使用watch。遗憾的是,这触发了一个众所周知的GDB错误(Sourceware#19365,Ubuntu#1573786,Redhat#1136403,…)-最近的GDB快照(如gdb-7.11.50.20160514)不正确-

(我现在很想避免GDB错误,也许是通过在#pragma GCC optimize ("-Og")之前有我自己的memsetmemcpy例程;但这看起来太过分了)

值得一提的是,崩溃消息由以下代码给出(在我的melt-runtime.h的第900行附近):

static inline int
melt_magic_discr (melt_ptr_t p)
{
  if (!p)
    return 0;
#if MELT_HAVE_DEBUG > 0 || MELT_HAVE_RUNTIME_DEBUG > 0
  if (MELT_UNLIKELY(!p->u_discr))
    {
      /* This should never happen, we are asking the discriminant of a
      not yet filled, since cleared, memory zone. */
      melt_fatal_error
      ("corrupted memory heap with null discriminant in %p; GC#%ld",
       (void*) p, melt_nb_garbcoll);
    }
#endif /*MELT_HAVE_DEBUG or MELT_HAVE_RUNTIME_DEBUG */
  gcc_assert (p->u_discr != NULL);
  return p->u_discr->meltobj_magic;
}    

我的猜测是,该漏洞可能是一个围绕"判别式"(每个MELT值中的一种"类型"、"类"或"元数据"字段)转发的GC漏洞,在这种判别式仍在年轻一代的罕见情况下。。。添加一些代码来避免这种情况确实会使错误稍后发生,但我一点也不确定。

欢迎任何与实际虚拟地址相关的调试heisenbug的线索或建议(因此对ASLR来说是明智的!)

我甚至添加了一些初始化代码,以便能够可选地mmapsbrk几个无用的兆字节,希望"重现"mmap(由MELT及其GC使用的calloc调用)给出的随机地址。这还没用!

我在Smalltalk垃圾收集器中使用的方法是在每个GC之前复制堆,并在副本中执行GC,然后在副本崩溃时重复进行调试。如果像我这样的系统是用高级oo语言开发的,那么这相对来说是微不足道的;复制堆只是复制包括VM模拟的对象的图(并且在模拟中堆在单个大字节数组中)。

在你的环境中应用这项技术可能会更具挑战性,但也不是不可能。让我在这里画一下。。。

我将把您尝试调试的进程称为"master",并将那些被克隆以尝试GC for it的进程称之为子进程。

在master中执行GC之前,执行fork并让子级执行GC,在子级中运行泄漏检查程序并退出,退出状态反映GC是否成功。如果子级成功,则主级继续执行其自己的GC。否则它会循环,生成重复失败GC的子级。然后调试子级。

孩子需要在两个州出生。每个GC中的初始启动只是运行GC并以成功状态退出。我们现在知道会失败的后续fork可以进入等待状态,这样您就可以将gdb附加到子级。

我称之为"旅鼠调试",因为在调试崩溃之前,可以让任意数量的克隆跳过悬崖。如果你能做到这一点,请告诉我。

最新更新