double freeバグのデバッグ.

昨日久々に,double freeのバグに当たったので,それのデバッグ方法をメモ書き.

こんな感じで,2重にfreeするバグを持ったプログラムがあったとして,
サンプルなので,原因は簡単に分かりますけど・・・

[masami@moonlight:~]% cat double_free.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>

char *ptr;

void handler(int signo)
{
	printf("%s: %s\n", __FUNCTION__, ptr);
	free(ptr);
}

int main(int argc, char **argv)
{
	struct sigaction act;
	memset(&act, 0, sizeof(act));

	ptr = malloc(32);
	if (!ptr) {
		perror("malloc");
		exit(-1);
	}

	if (argc == 1)
		strcpy(ptr, "Hello World");
	else
		strncpy(ptr, argv[1], 31);

	act.sa_handler = &handler;

	sigaction(SIGALRM, &act, NULL);
	alarm(5);
	sleep(5);

	free(ptr);

	return 0;
}

これを実行すると,こんな感じでglibcが2重freeを検出してくれます.

[masami@moonlight:~]% ./a.out
handler: Hello World
*** glibc detected *** ./a.out: double free or corruption (fasttop): 0x0000000000f34010 ***
======= Backtrace: =========
/lib/libc.so.6[0x7fca40d3ad56]
/lib/libc.so.6(cfree+0x6c)[0x7fca40d3f9bc]
./a.out[0x400916]
/lib/libc.so.6(__libc_start_main+0xfd)[0x7fca40ce8abd]
./a.out[0x400729]
======= Memory map: ========
00400000-00401000 r-xp 00000000 08:01 1818                               /home/masami/a.out
00600000-00601000 rw-p 00000000 08:01 1818                               /home/masami/a.out
00f34000-00f55000 rw-p 00000000 00:00 0                                  [heap]
7fca3c000000-7fca3c021000 rw-p 00000000 00:00 0 
7fca3c021000-7fca40000000 ---p 00000000 00:00 0 
7fca40ab4000-7fca40aca000 r-xp 00000000 08:01 131765                     /lib/libgcc_s.so.1
7fca40aca000-7fca40cc9000 ---p 00016000 08:01 131765                     /lib/libgcc_s.so.1
7fca40cc9000-7fca40cca000 rw-p 00015000 08:01 131765                     /lib/libgcc_s.so.1
7fca40cca000-7fca40e14000 r-xp 00000000 08:01 138066                     /lib/libc-2.10.2.so
7fca40e14000-7fca41014000 ---p 0014a000 08:01 138066                     /lib/libc-2.10.2.so
7fca41014000-7fca41018000 r--p 0014a000 08:01 138066                     /lib/libc-2.10.2.so
7fca41018000-7fca41019000 rw-p 0014e000 08:01 138066                     /lib/libc-2.10.2.so
7fca41019000-7fca4101e000 rw-p 00000000 00:00 0 
7fca4101e000-7fca4103b000 r-xp 00000000 08:01 131120                     /lib/ld-2.10.2.so
7fca41225000-7fca41227000 rw-p 00000000 00:00 0 
7fca41236000-7fca4123a000 rw-p 00000000 00:00 0 
7fca4123a000-7fca4123b000 r--p 0001c000 08:01 131120                     /lib/ld-2.10.2.so
7fca4123b000-7fca4123c000 rw-p 0001d000 08:01 131120                     /lib/ld-2.10.2.so
7fff87315000-7fff8732a000 rw-p 00000000 00:00 0                          [stack]
7fff8737d000-7fff8737e000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
zsh: abort (core dumped)  ./a.out

この出力から,どこで2回目のfreeが呼ばれたかを探すには,Backtraceを見ます.

======= Backtrace: =========
/lib/libc.so.6[0x7fca40d3ad56]
/lib/libc.so.6(cfree+0x6c)[0x7fca40d3f9bc]
./a.out[0x400916]
/lib/libc.so.6(__libc_start_main+0xfd)[0x7fca40ce8abd]
./a.out[0x400729]

トレースは上記なので,glibcに入る前に呼ばれたコードは「./a.out[0x400916]」です.

masami@moonlight:~]% objdump -D a.out | less
  000000000040081e <main>:
  40081e:       55                      push   %rbp
  40081f:       48 89 e5                mov    %rsp,%rbp
  400822:       48 81 ec b0 00 00 00    sub    $0xb0,%rsp
〜中略〜
  4008f3:       bf 05 00 00 00          mov    $0x5,%edi
  4008f8:       e8 d3 fd ff ff          callq  4006d0 <alarm@plt>
  4008fd:       bf 05 00 00 00          mov    $0x5,%edi
  400902:       e8 99 fd ff ff          callq  4006a0 <sleep@plt>
  400907:       48 8b 05 5a 04 20 00    mov    0x20045a(%rip),%rax        # 600d68 <ptr>
  40090e:       48 89 c7                mov    %rax,%rdi
  400911:       e8 7a fd ff ff          callq  400690 <free@plt>
  400916:       b8 00 00 00 00          mov    $0x0,%eax
  40091b:       c9                      leaveq 
  40091c:       c3                      retq   

そうすると,直前の0x400911でfreeを呼んでいる箇所があるので,2回目のfreeはこいつだなとわかります.

  400911:       e8 7a fd ff ff          callq  400690 <free@plt>

サンプルコードの場合は,シグナルハンドラ内でfree()するか,main()の最後にfree()するかで解決するのですが,
そうは簡単にいかない場合もあると思うので,一番簡単な解決策はfreeしたらNULLを代入しておくのが良いんじゃないかと.
free(NULL)は何もしないので(manにも書かれてます),比較的簡単・安全かと.

昨日当たったバグは,ちょっと厄介で,全てのfree()呼び出し後にNULLを入れたんですが,
バッファを獲得する処理でfree済みのアドレスが取得されて,それをfreeしたために2重freeで落ちてる感じでした. 
ちゃんと調べると面倒そうだったので(仕事でもないし),free()を呼ばないという手抜きな方法を使っちゃいましたけどw