Отладка neovim, переполнение буфера, обнаруженное OS X 10.9 libc, нуждается в лучшем способе отладки

Прежде всего, немного контекста: я пытаюсь устранить проблему, возникающую с neovim, я не уверен, что это также происходит с обычным vim, но это не так уж важно.

Несмотря на то, что репортер использует Linux, а я использую OSX 10.9, я смог получить «похожее» поведение, используя определенные компиляторы + флаги:

Когда я использую gcc 4.8.2 или gcc 4.9 (dev) в сочетании даже с небольшой оптимизацией, укреплением и защитой от разрушения стека, neovim падает при запуске.

$ edit CMakeLists.txt
$ ... -Wall -O1 -g -g3 -ggdb -mtune=generic -pipe -fstack-protector --param=ssp-buffer-size=4 -D_FORTIFY_SOURCE=2 -Wextra -pedantic -Wno-unused-parameter -std=gnu99 ...

$ make clean && make cmake CMAKE_EXTRA_FLAGS="-DCMAKE_C_COMPILER=/usr/local/bin/gcc-4.8" && make

Я пытался отладить его с помощью lldb (gdb, похоже, не дает никаких символов даже после написания кода). Я дошел до этого:

скомпилировано с gcc 4.9:

➜  neovim git:(fortify-and-stack-protector) ✗ lldb ./build/bin/nvim
Current executable set to './build/bin/nvim' (x86_64).
(lldb) run src/eval.c
Process 1295 launched: './build/bin/nvim' (x86_64)
Process 1295 stopped
* thread #1: tid = 0x242b4f, 0x00007fff93f34866 libsystem_kernel.dylib`__pthread_kill + 10, queue = 'com.apple.main-thread, stop reason = signal SIGABRT
    frame #0: 0x00007fff93f34866 libsystem_kernel.dylib`__pthread_kill + 10
libsystem_kernel.dylib`__pthread_kill + 10:
-> 0x7fff93f34866:  jae    0x7fff93f34870            ; __pthread_kill + 20
   0x7fff93f34868:  movq   %rax, %rdi
   0x7fff93f3486b:  jmpq   0x7fff93f31175            ; cerror_nocancel
   0x7fff93f34870:  ret
(lldb) bt
* thread #1: tid = 0x242b4f, 0x00007fff93f34866 libsystem_kernel.dylib`__pthread_kill + 10, queue = 'com.apple.main-thread, stop reason = signal SIGABRT
    frame #0: 0x00007fff93f34866 libsystem_kernel.dylib`__pthread_kill + 10
    frame #1: 0x00007fff91b6435c libsystem_pthread.dylib`pthread_kill + 92
    frame #2: 0x00007fff8ce68b1a libsystem_c.dylib`abort + 125
    frame #3: 0x00007fff8ce68c91 libsystem_c.dylib`abort_report_np + 181
    frame #4: 0x00007fff8ce8c860 libsystem_c.dylib`__chk_fail + 48
    frame #5: 0x00007fff8ce8c830 libsystem_c.dylib`__chk_fail_overflow + 16
    frame #6: 0x00007fff8ce8ca7f libsystem_c.dylib`__strcpy_chk + 83
    frame #7: 0x000000010002e1a6 nvim`call_user_func [inlined] add_nr_var(nr=1, name=<unavailable>, v=<unavailable>, dp=<unavailable>) + 42 at eval.c:18744
    frame #8: 0x000000010002e17c nvim`call_user_func(fp=0x000000010030bd30, argcount=0, argvars=0x00007fff5fbfed80, rettv=0x00007fff5fbfef50, firstline=1, lastline=1, selfdict=0x0000000000000000) + 425 at eval.c:18455
    frame #9: 0x000000010002ef33 nvim`call_func(funcname=<unavailable>, len=<unavailable>, rettv=0x00007fff5fbfef50, argcount=0, argvars=0x00007fff5fbfed80, firstline=1, lastline=1, doesrange=0x00007fff5fbfef44, evaluate=1, selfdict=0x0000000000000000) + 717 at eval.c:7363
    frame #10: 0x0000000100032d1a nvim`get_func_tv(name=0x000000010030be20, len=9, rettv=0x00007fff5fbfef50, arg=0x00007fff5fbfef48, firstline=1, lastline=1, doesrange=0x00007fff5fbfef44, evaluate=1, selfdict=0x0000000000000000) + 340 at eval.c:7222
    frame #11: 0x000000010003673e nvim`ex_call(eap=0x00007fff5fbff190) + 475 at eval.c:3086
    frame #12: 0x000000010005634b nvim`do_cmdline(cmdline=<unavailable>, fgetline=0x00000001000494e3, cookie=0x00007fff5fbff790, flags=7) + 13602 at ex_docmd.c:2103
    frame #13: 0x0000000100049d52 nvim`do_source(fname=0x000000010017bb3b, check_other=<unavailable>, is_vimrc=<unavailable>) + 1615 at ex_cmds2.c:2695
    frame #14: 0x00000001001702c3 nvim`main + 251 at main.c:2009
    frame #15: 0x00000001001701c8 nvim`main(argc=<unavailable>, argv=<unavailable>) + 5152 at main.c:1919
    frame #16: 0x00007fff8b32d5fd libdyld.dylib`start + 1
(lldb) frame variable
(lldb) frame info
frame #0: 0x00007fff93f34866 libsystem_kernel.dylib`__pthread_kill + 10
(lldb) frame select 7
frame #7: 0x000000010002e1a6 nvim`call_user_func [inlined] add_nr_var(nr=1, name=<unavailable>, v=<unavailable>, dp=<unavailable>) + 42 at eval.c:18744
   18741     */
   18742    static void add_nr_var(dict_T *dp, dictitem_T *v, char *name, varnumber_T nr)
   18743    {
-> 18744      STRCPY(v->di_key, name);
   18745      v->di_flags = DI_FLAGS_RO | DI_FLAGS_FIX;
   18746      hash_add(&dp->dv_hashtab, DI2HIKEY(v));
   18747      v->di_tv.v_type = VAR_NUMBER;

скомпилировано с gcc 4.8.2:

➜  neovim git:(fortify-and-stack-protector) ✗ lldb ./build/bin/nvim
Current executable set to './build/bin/nvim' (x86_64).
(lldb) rune
error: 'rune' is not a valid command.
(lldb) run
Process 3242 launched: './build/bin/nvim' (x86_64)
Process 3242 stopped
* thread #1: tid = 0x2454cb, 0x00007fff93f34866 libsystem_kernel.dylib`__pthread_kill + 10, queue = 'com.apple.main-thread, stop reason = signal SIGABRT
    frame #0: 0x00007fff93f34866 libsystem_kernel.dylib`__pthread_kill + 10
libsystem_kernel.dylib`__pthread_kill + 10:
-> 0x7fff93f34866:  jae    0x7fff93f34870            ; __pthread_kill + 20
   0x7fff93f34868:  movq   %rax, %rdi
   0x7fff93f3486b:  jmpq   0x7fff93f31175            ; cerror_nocancel
   0x7fff93f34870:  ret
(lldb) bt
* thread #1: tid = 0x2454cb, 0x00007fff93f34866 libsystem_kernel.dylib`__pthread_kill + 10, queue = 'com.apple.main-thread, stop reason = signal SIGABRT
    frame #0: 0x00007fff93f34866 libsystem_kernel.dylib`__pthread_kill + 10
    frame #1: 0x00007fff91b6435c libsystem_pthread.dylib`pthread_kill + 92
    frame #2: 0x00007fff8ce68b1a libsystem_c.dylib`abort + 125
    frame #3: 0x00007fff8ce68c91 libsystem_c.dylib`abort_report_np + 181
    frame #4: 0x00007fff8ce8c860 libsystem_c.dylib`__chk_fail + 48
    frame #5: 0x00007fff8ce8c830 libsystem_c.dylib`__chk_fail_overflow + 16
    frame #6: 0x00007fff8ce8ca7f libsystem_c.dylib`__strcpy_chk + 83
    frame #7: 0x000000010002a969 nvim`eval_init + 129 at eval.c:868
    frame #8: 0x0000000100089ffd nvim`main(argc=1, argv=0x00007fff5fbffa58) + 140 at main.c:175
    frame #9: 0x00007fff8b32d5fd libdyld.dylib`start + 1
    frame #10: 0x00007fff8b32d5fd libdyld.dylib`start + 1
(lldb) frame select 7
frame #7: 0x000000010002a969 nvim`eval_init + 129 at eval.c:868
   865
   866    for (i = 0; i < VV_LEN; ++i) {
   867      p = &vimvars[i];
-> 868      STRCPY(p->vv_di.di_key, p->vv_name);
   869      if (p->vv_flags & VV_RO)
   870        p->vv_di.di_flags = DI_FLAGS_RO | DI_FLAGS_FIX;
   871      else if (p->vv_flags & VV_RO_SBX)

Чтобы эта проблема появилась, должна быть некоторая оптимизация, однако это означает, что компилятор будет встраивать вещи и отбрасывать аргументы, что раздражает и не позволяет мне сразу увидеть самые важные вещи. Могу ли я попробовать комбинацию флагов, которая сохранит проблему, но улучшит отладку?

.

STRCPY(v->di_key, name);
// I think gcc/clang replace this with:
__strcpy_chk(v->di_key, name, SOME_MAGIC_SIZE);

Я надеюсь, что некоторые из гуру по переполнению стека могут дать мне несколько советов / подсказок о том, что мне делать дальше!

EDIT: мне удалось скомпилировать с -Og и gcc 4.8.2 и все еще вызывать ошибку, надеюсь, это даст больше информации.


person Aktau    schedule 03.03.2014    source источник


Ответы (1)


Итак, я не мог удержаться от дальнейшего копания и, наконец, пришел к идее посмотреть на стек + регистры при вводе страшного __strcpy_chk, где я нашел:

(lldb) register read
General Purpose Registers:
       rax = 0x0000000000000057
       rbx = 0x00000001001d3900  vimvars
       rcx = 0x6300f1e7a96add52
       rdx = 0x0000000000000001 /* this is probably the size parameter */
       rdi = 0x00000001001d3919  vimvars + 25
       rsi = 0x000000010016730e  "count"

Итак, gcc сделал вывод, что размер параметра dst равен 1, и передает его в __strcpy_chk. Итак, из-за -D_FORTIFY_SOURCE=2, gcc заменяет вызовы общеизвестных функций их безопасными вариантами, когда может вычислить размер. Как мы увидим, здесь дело обстоит именно так: параметр dst является полем vv_di.di_key этой структуры:

static struct vimvar {
  char        *vv_name;         /* name of variable, without v: */
  dictitem_T vv_di;             /* value and name for key */
  char vv_filler[16];           /* space for LONGEST name below!!! */
  char vv_flags;                /* VV_COMPAT, VV_RO, VV_RO_SBX */
}

struct dictitem_S {
  typval_T di_tv;               /* type and value of the variable */
  char_u di_flags;              /* flags (only used for variable) */
  char_u di_key[1];             /* key (actually longer!) */
}

Который имеет размер 1. Gcc, конечно, не понимает, что это было преднамеренно: флаг vv_filler предназначен для хранения фактической строки. Вот почему в комментарии написано на самом деле дольше. vv_filler следует сразу за di_key в структуре.

Я не могу винить в этом компилятор, в этом есть смысл. Кто-то из проекта neovim занят перестройкой интерпретатора, чтобы он стал переводчиком с VimL на lua. «Глупое» исправление на данный момент состоит в том, чтобы либо отключить усиление источника, либо понизить его уровень до -D_FORTIFY_SOURCE=1, что должно помешать gcc делать такие предположения. В идеале, однако, мы могли бы исправить это, сохранив -D_FORTIFY_SOURCE=2 и не снижая производительность (конечно, vimvar — часто используемая структура).

Но у меня есть дополнительные вопросы. Является ли кодовая база vim законной? Было несколько дискуссий на трекере проблем neovim, где некоторые комментаторы предположили, что полагаться на это было совершенно неопределенно. Один аргумент, который мне показался интересным, заключался в том, что между концом struct dictitem_S и vv_filler может быть пробел (заполнение). Комментатор утверждал, что использование этого пространства было незаконным (UB). Чтение соответствующих документов C99 не дает полной ясности в этом конкретном случае. Взлом структуры разрешен, но мы не были уверены, разрешено ли это для вложенных структур. то есть: struct hack появляется в конце dictitem_S, но определенно не в конце содержащей его структуры (vimvar).

person Aktau    schedule 03.03.2014