2. Команда (Google RU-MSK)
Константин Серебряный (TL)
Дмитрий Вьюков
Тимур Исходжанов
Сергей Матвеев
Александр Потапенко
Алексей Самсонов
Евгений Степанов
4. В мире много кода на C и C++
● Операционные системы
● Браузеры (Chromium, Firefox, Safari)
● Виртуальные машины (Java)
● Базы данных (MySQL)
● Backend веб-сервисов (поиск)
● Веб-серверы (Apache, nginx)
5. Ошибки работы с памятью в C/C++
● Кодеки и рендереры (ffmpeg, pdf)
● Использование освобождённой памяти
(use-after-free)
● Использование неинициализированной
памяти (uninitialized-memory-read)
● Утечки памяти (memory leaks)
● Неверный порядок
инициализации/уничтожения глобальных
объектов
6. Ошибки работы с памятью в C/C++
● Выход за границы буфера (out-of-bound
access)
● Использование освобождённой память
(use-after-free)
● Использование неинициализированной
памяти (uninitialized-memory-read)
● Утечки памяти (memory leaks)
● Неверный порядок
инициализации/уничтожения глобальных
объектов
7. Выход за границы буфера (стек)
void authorize() {
...
char username[128];
gets(username); // 128 bytes should be
// enough for everyone.
...
}
10. Всегда в моде: racy use-after-free
Поток 1
void DeleteEntries() {
have_entries = false;
delete entries;
}
Поток 2
Entry *GetEntry(int i) {
if (have_entries)
return entries[i];
return 0;
}
11. Инструменты для поиска ошибок
● Valgrind/Memcheck
● DynamoRIO/Dr. Memory
● Purify
● Insure++
● Mudflap
● Electric Fence / Page Heap / Guard Malloc
Проблемы:
● Замедление программы (в 10 и более раз)
● Поиск ошибок только для динамически
выделенной памяти (heap)
12. Статический анализ программ
● Формальная верификация?
● Clang Static Analyzer?
Проблемы:
● Баланс между эффективностью и
полезностью
● Баланс между false negative и false
positive
13. Есть ли в программе ошибка?
int array[10] = {...};
int main(int argc, char *argv[]) {
return array[2 + argc];
}
14.
15. Пример отчёта об ошибке (1)
int global_array[100] = {-1};
int main(int argc, char **argv) {
return global_array[argc + 100];
}
==10538== ERROR: AddressSanitizer global-buffer-overflow
READ of size 4 at 0x000000415354 thread T0
#0 0x402481 in main a.cc:3
<...>
0x000000415354 is located 4 bytes to the right of global
variable 'global_array' (0x4151c0) of size 400
16. Пример отчёта об ошибке (2)
int main(int argc, char **argv) {
int stack_array[100];
stack_array[1] = 0;
return stack_array[argc + 100];
}
==10589== ERROR: AddressSanitizer stack-buffer-overflow
READ of size 4 at 0x7f5620d981b4 thread T0
#0 0x4024e8 in main a.cc:4
Address 0x7f5620d981b4 is located at offset 436 in frame
<main> of T0's stack:
This frame has 1 object(s):
[32, 432) 'stack_array'
17. Пример отчёта об ошибке (3)
int main(int argc, char **argv) {
int *array = new int[100];
int res = array[argc + 100];
delete [] array;
return res;
}
==10565== ERROR: AddressSanitizer heap-buffer-overflow
READ of size 4 at 0x7fe4b0c76214 thread T0
#0 0x40246f in main a.cc:3
0x7fe4b0c76214 is located 4 bytes to the right of 400-
byte region [0x7fe..., 0x7fe...)
allocated by thread T0 here:
#0 0x402c36 in operator new[](unsigned long)
#1 0x402422 in main a.cc:2
18. Пример отчёта об ошибке (4)
int main(int argc, char **argv) {
int *array = new int[100];
delete [] array;
return array[argc];
}
==30226== ERROR: AddressSanitizer heap-use-after-free
READ of size 4 at 0x7faa07fce084 thread T0
#0 0x40433c in main a.cc:4
0x7faa07fce084 is located 4 bytes inside of 400-byte
region
freed by thread T0 here:
#0 0x4058fd in operator delete[](void*)
#1 0x404303 in main a.cc:3
previously allocated by thread T0 here:
#0 0x405579 in operator new[](unsigned long)
#1 0x4042f3 in main a.cc:2
19.
20. Что такое AddressSanitizer?
● Компиляторная инструментация
○ Проверка всех обращений к памяти
○ Добавление редзон("redzones") вокруг
глобальных и локальных переменных
● Runtime-библиотека
○ Аллокатор (выделение/освобождение памяти)
○ Отслеживание всех потоков
○ Печать отчётов об ошибках
○ Обёртки функций из стандартной библиотеки
21. Теневой байт (shadow byte)
Если адреса всех переменных кратны 8, то у любых
выровненных восьми байт есть 9 состояний:
0
7
6
5
4
3
2
1
-1
28. Интеграция в LLVM
● Clang (или другой фронтенд) генерирует
промежуточное представление
программы;
● ASan добавляет инструментацию;
● Бэкенд оптимизирует код и генерирует
объектный файл;
● Исполняемый файл линкуется с runtime-
библиотекой.
29. Runtime-библиотека
● Аллокатор
○ Добавление редзон вокруг блоков динамически
выделенной памяти
○ Карантин для освобождённой памяти
○ Хранение стека вызовов для каждой аллокации
● Печать отчётов об ошибках
● Перехват библиотечных функций
char bad_string[2] = {'h', 'i'};
int len = strlen(bad_string);
30. Производительность
● Замедление программы: 2x
● Использование памяти: 1,5-3x
● "Just works" для большинства тестов и
реальных программ
● Незначительное замедление для GUI-
программ (Chromium+ASan работает без
проблем)
31. Трофеи
1000+ найденных ошибок повсюду:
● Chromium, Firefox, Safari
● Сервисы Google
● Perl, PHP, MySQL, ffmpeg, webrtc, vim
● LLVM, GCC
Google заплатил $130000+ внешним
исследователям за security-ошибки,
найденные с помощью AddressSanitizer.
32. Что будет дальше?
● Поиск других видов ошибок (в ближайшее
время: поиск утечек памяти)
● Поиск ошибок в системных и
прекомпилированных библиотеках
● AddressSanitizer для ядра Linux
● AddressSanitizer для Windows (нужен
компилятор).
35. Что такое гонка?
Гонка происходит, когда два потока одновременно
обращаются к одной и той же переменной, и хотя бы
одно из обращений является записью.
Если в программе на C++ могут возникнуть гонки, её
поведение не определено.
Компилятор имеет право считать, что в программе нет
гонок.
36. Что напечатает этот код?
Поток 1
cout << x << "n";
x = 1;
cout << y << "n";
Поток 2
cout << y << "n";
y = 1;
cout << x << "n";
int x = 0, y = 0;
37. "Безвредные" гонки
● Синхронизация замедляет программу.
● Может возникнуть желание допустить
гонки на "не очень важных" данных.
● Не нужно этого делать. В C++ не может
быть "безвредных" (benign) гонок.
H-J. Boehm "How to miscompile programs with "benign"
data races"
D. Vyukov "Benign data races: what could possibly go
wrong?"
38.
39. Пример "безвредной гонки"
long long
get_value() {
return value;
}
void set_value(
long long x) {
value = x;
}
Является ли 64-битное чтение / запись атомарной
операцией?
40. Пример "безвредной гонки" (2)
...
int my_counter = counter; // read global
if (my_counter > my_old_counter) {
my_action = print_stats;
}
...
if (my_counter > my_old_counter) {
run_action(my_action);
}
41. Пример "безвредной гонки" (3)
my_action = launch_nuclear_rocket;
...
int my_counter = counter; // read global
if (my_counter > my_old_counter) {
my_action = print_stats;
}
...
my_counter = counter;
if (my_counter > my_old_counter) {
run_action(my_action);
}
43. Метод-лжец
// Returns the user with a given ID
User& getUser(int id) {
MutexLock(&mutex);
CHECK(0 <= id && id < users.size());
return users[id];
}
44. Метод-лжец
// Returns the user with a given ID
User& getUser(int id) {
MutexLock(&mutex);
CHECK(0 <= id && id < users.size());
return users[id];
}
45. Что не так с этим кодом?
class AbstractAction {
public:
AbstractAction(Executor *executor) {
executor->add(this);
...
}
...
}
46. Что не так с этим кодом?
class AbstractAction {
public:
AbstractAction(Executor *executor) {
executor->add(this);
...
}
...
virtual void Run() = 0;
}
47. Гонка на vptr
class AbstractAction {
public:
AbstractAction(Executor *executor) {
executor->add(this);
// переключение контекста
}
...
virtual void Run() = 0;
}
Pure virtual call!
48. ThreadSanitizer спешит на помощь
WARNING: ThreadSanitizer: data race on vptr (ctor/dtor vs
virtual call)
Read of size 8 at 0x7fff103327f0 by thread T3:
#0 ExecutorThread::RunAction() executor.cc:1112
#1 ExecutorThread::Start() executor.cc:1283
Previous write of size 8 at 0x7fff103327f0 by main
thread:
#0 MyAction::MyAction()vptr_race.cc:48
#1 main vptr_race.cc:59
Location is stack of main thread.
49. Что такое ThreadSanitizer?
● Компиляторная инструментация
○ Проверка всех обращений к памяти
○ Отслеживание событий: вход/выход в функцию,
обновление vptr, атомарные операции
● Runtime-библиотека
○ Аллокатор (выделение/освобождение памяти)
○ Отслеживание всех потоков
○ Печать отчётов об ошибках
○ Обёртки функций из стандартной библиотеки
○ Поддержка синхронизационных примитивов
53. Runtime-библиотека
● Проверка всех доступов в память
● Выделение/освобождение памяти
● Поддержка синхронизации (мьютексы,
атомарные операции)
● Перехват функций работы с потоками
(libpthread)
● Перехват функций из стандартной
библиотеки
● Печать отчётов об ошибках
54. Абстрактное время потока
Увеличивается после каждого события:
void foo() { // time = 1
Lock(&mutex); // time = 2
x = 1; // time = 3
Unlock(&mutex); // time = 4
} // time = 5
55. Векторные часы (vector clock)
VC[1..N], где VC[i] - время i-го потока.
Векторные часы есть у каждого потока:
VC[1..N], где VC[i] - время i-го потока в
момент последней синхронизации с ним.
Векторные часы есть у каждого мьютекса:
VC[1..N], VC[i] - время i-го потока, когда
он в последний раз отпустил мьютекс.
57. Обновление векторных часов (2)
Взятие мьютекса M в потоке T:
для всех i:
T->VC[i] = max(T->VC[i], M->VC[i]);
Освобождение мьютекса M в потоке T:
для всех i:
M->VC[i] = max(M->VC[i], T->VC[i]);
58. Как кодировать обращение к
памяти?
Обращение к выровненным 8 байтам
адресного пространства кодируется через:
● ID потока (TID)
● Время этого потока (TS)
● позиция (0..7) и размер (1,2,4,8)
обращения
● является ли доступ чтением или
записью?
Вся эта информация помещается в 8 байт.
64. Как узнать стек вызовов для
каждого обращения к памяти?
● Циклическая очередь всех событий в
каждом потоке:
○ доступ к памяти
○ вход/выход из функции
○ взятие/освобождение мьютекса
● "Повторение" событий перед печатью
отчёта:
○ Через некоторое время события "забываются"
○ Неограниченный размер стека вызовов в отчёте
○ Отчёт содержит множество мьютексов, взятых в
каждом потоке.
65. Результаты
● Замедление программы: 4-10x (в 10 раз
быстрее аналогов).
● Использование памяти: 5-8x
● 600+ найденных ошибок
● Работает для больших серверных
приложений
66. Что будет дальше?
● Портирование на другие системы (сейчас
- только 64-битный Linux).
● Поиск ошибок в системных и
прекомпилированных библиотеках
● Больше оптимизаций, больше
пользователей
● Поиск гонок в коде на Java