]> Untitled Git - articles.git/blob
5a4c8204c81fff10a6646470aafcadf297e1c9a1
[articles.git] /
1 # Подготовка окружения для исследуемого процесса с испольнованием btrfs
2
3 **Update:** По состоянию на 2025 год, коллеги из БазАльта предложили более совершенный способ переключения между снепшотами, основанный на обращении к соответствующим системным вызовам ядра прямо из кода исследуемой программы, в самом ее начале. Это позволяет реализовать фаззинг со снепшотами каталога данных на основе свободного AFL++. Описание подхода находится [Здесь](https://www.altlinux.org/PostgreSQL16_fuzzing_technique) ([WebArchive](http://web.archive.org/web/20241203093118/https://www.altlinux.org/PostgreSQL16_fuzzing_technique))
4
5
6 ---
7
8 Часто при фаззинге сложных проектов может оказаться, что для нормальной работы каждому экземпляру исследуемой программы нужна собственная директория с данными, которую не плохо бы перед каждым новым прогоном исследуемой программы возвращать в исходное состояние. В данном посте в формате "заметок" я расскажу как мне удалось сделать восстановление окружения при фаззинге postgres'а, используя btrfs под фаззером Crusher.
9
10 Для фаззига postgres'а система восстановления контекста чрезвычайно важна. Полноценный postgres не может работать без своего личного хранилища, и при этом меняет его практически на каждый чих. Без возвращения хранилища к исходному состоянию при тестировании, ни о какой воспроизводимости и речи быть не может. Поэтому как только заходит речь о фаззинг-тестировании postgres'а в сборе, необходимо сразу думать о системе восстановления контекста. 
11
12 ## btrfs -- файловая система со снепшотами
13
14 В качестве framework'а для системы восстановления контекста была выбрана файловая система btrfs. Btrfs позволяет эффективно работать со снепшотами на уровне директорий. А именно, позволяет в рамках файловой системы получить клон указанной директории с минимальными затратами. Клонирование происходит на логическом уровне, данные по факту не копируются, и только лишь изменения внесенные в копию или в оригинал занимают дополнительное место на диске. Все это позволяет экономить время и дисковое пространство.
15
16 Фаззинг, при его промышленном применении, имеет практический смысл, будучи запущенным во много потоков на многих процессорах. Для каждого инстанса фаззера, а точнее для исследуемой программы запущенной этим фаззером, предполагается создать свой собственный снепшот с уникальным именем, содержащий данные необходимые для запуска исследуемой программы, и возвращать этот снепшот к исходному состоянию перед каждым новым прогоном исследуемой программы, чтобы следы  оставленные в хранилище предыдущими прогонами не оказывали влияния на последующее..  
17
18 Мной был написан скрипт `snapshooter.pl` (код приведен в конце поста) реализующий действия  со снепшотами, необходимые для фаззинга. Скрипту первым параметром передается имя команды и далее параметры которые нужны команде для работы.
19
20 Ниже приведены команды и их описания:
21
22 ###### `init MOUNT_POINT IMAGE_FILE IMAGE_SIZE IMAGE_COUNT`
23
24 Принимает 4 аргумента: точка монтирования `MOUNT_POINT`, путь к создаваемому образу файловой системы `IMAGE_FILE`, предполагаемый размер одного снепшота `IMAGE_SIZE` и ожидаемое количество снепшотов `IMAGE_COUNT`.
25
26 Скрипт отмонтирует то, что было примантировано к точки монтирования, создаст новый образ файловой системы, примонтирует его к точке монтирования, и создаст в файловой системе директорию `_reference`.
27
28 В эту директорию следует скопировать "эталонный" снепшот, на базе которого будут строиться снепшоты для всех экземпляров
29
30 ###### `clone MOUNT_POINT SNAPSHOT_NAME`
31
32 Принимает два аргумента: точку монтирования `IMAGE_COUNT` и имя снэпшота `SNAPSHOT_NAME`.
33
34 Команда `clone` клонирует эталонный снепшот в директорию `[SNAPSHOT_NAME].base`. Если есть потребность внести какие-то специфичные для инстанса изменения в снепшот, то это следует сделать в этой директории. В дальнейшем перед каждым запуском исследуемой программы рабочий снепшот будет приводится к "базововму" состоянию.
35
36 ###### `reset MOUNT_POINT SNAPSHOT_NAME`
37
38 Принимает два аргумента: точку монтирования `MOUNT_POINT`и имя снэпшота `SNAPSHOT_NAME`.
39
40 Команда `reset` удаляет снепшот `SNAPSHOT_NAME`, и создает новый с таким же именем клонируя  базовый снепшот `[SNAPSHOT_NAME].base`. Таким образом происходит восстановление целевого снепшота исходное состояние.
41
42 В дальнейшем, для ускорения работы, планируется образ файловой системы со снепшотами перенести с диска в память, но я пока еще не разбирался как это делается.
43
44 ## Crusher
45
46 В Crusher 2.11.0 появились инструменты позволяющие инициировать подготовку, и восстановления окружения для запускаемых процессов. К сожалению для того чтобы задействовать эти инструменты необходимо написание скриптов на языке python (и ни на каком другом), а я этот python не умею, и честно говоря не очень хочу уметь. К счастью коллеги снабдили меня обертками которые позволяют из непонятного питона запускать понятные `.sh` и прочие скрипты. Поэтому конструкция получилась двухсоставная. 
47
48 Предложенное решение использует два "хука" предоставляемые крашером. Первый хук дергается при создании нового инстанса фаззера (см. `--configurator-script` в документации), и используется для создания нового снепшота с данными  хранилища, которыми будет пользоваться исследуемая программа. Второй хук дергается перед каждым новым прогоном исследуемой программы (см. `--environment-plugin` в документации), и используется для того чтобы вернуть снепшот с хранилищем в исходное состояние (ибо есть шанс что при предыдущих запусках он мог быть иземенен)  
49
50 ### Подготовка окружения
51
52 Изначальная подготовка окружения осуществляется через скрипт `conf.py`:
53
54 ```python
55 import json
56 import os
57 import sys
58 import traceback
59 import subprocess
60
61 def transform_options(ops_json):
62     try:
63         jops = json.loads(ops_json)
64         instance_name = jops['configuration']['instance_name']
65         this_dir = os.path.dirname(__file__)
66         script_path = this_dir + '/init_script.sh'
67         pr = subprocess.Popen([script_path, instance_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
68         pr.wait()
69         assert pr.returncode == 0
70         output = pr.stdout.read()
71         for line in output.split():
72             ls = line.split('=')
73             assert len(ls) == 2
74             var_name = ls[0].strip()
75             var_value = ls[1].strip()
76             os.environ[var_name] = var_value
77         return json.dumps(jops)
78     except Exception as ex:
79         print("EXCEPTION!")
80         traceback.print_exc()
81         return None
82 ```
83 Который в свою очередь вызывает скрипт `init_script.sh`:
84
85 ```bash
86 #!/bin/bash
87
88 INSTANCE_NAME=$1
89 THIS_DIR=$(dirname $0)
90
91 sudo ./snapshooter.pl clone mnt "${INSTANCE_NAME}"  1>&2  || exit 1
92 sudo ./snapshooter.pl reset mnt "${INSTANCE_NAME}"  1>&2  || exit 1
93
94 echo "PGDATA=${THIS_DIR}/mnt/$INSTANCE_NAME"
95 ```
96
97 Скрипту `init_script.sh` в качестве аргумента передается уникальное имя инстанса фаззера, это имя скрипт использует в качестве имени создаваемого снепшота.  Кроме того на `STDOUT` скрипт выводит пары `NAME=VALUE` переменных окружения которые должны быть установлены при запуске. В нашем случае мы через переменную окружения сообщаем постгресу где находится его хранилище. Вывод скрипта анализируется оберткой  `env.py` и значения переменных окружения передаются фаззеру для последующего применения.
98
99 При запуске фаззера необходимо указать расположение скрипта `conf.py` через параметр `--configurator-script `
100
101 ### Восстановление окружения
102 Идея та же, что и с созданием окружения. Питоновская обертка `env.py`:
103
104 ```python
105 import json
106 import os
107
108 error_msg = '...'
109
110 def get_error():
111     return error_msg
112
113 instance_name = None
114
115 def init(json_options):
116     jops = json.loads(json_options)
117     global instance_name
118     instance_name = jops['configuration']['instance_name']
119     return True
120
121 def finish():
122     return True
123
124 def setup():
125     this_dir = os.path.dirname(__file__)
126     script_path = this_dir + '/run_script.sh'
127     cmd = script_path + ' ' + instance_name
128     r = os.system(cmd)
129     return r == 0
130
131 def teardown():
132     return True
133 ```
134 И `.sh` скрипт выполняющий содержательную работу:
135 ```bash
136 #!/bin/bash
137
138 INSTANCE_NAME=$1
139
140 sudo ./snapshooter.pl reset mnt "${INSTANCE_NAME}"  1>&2 || exit 1
141 ```
142 В данном случае мы получаем имя инстанса в качестве аргумента и восстанавливаем снепшот с этим именем в исходное состояние.
143
144 При запуске фаззинга расположение файла `env.py` указывается через параметр конфигурации `--environment-plugin`
145
146 ### Запуск фаззинга
147 Перед запуском фаззинга мы должны инициализировать хранилище снепшотов, раздать эталонному снепшоту правильные права (постгрес требует чтобы хранилище не было доступно на чтение кому либо кроме постгреса) и заполнить правильными данными. Заранее подготовленный снимок хранилища у меня лежит в директории `etalon`, я его просто копирую. Ну а дальше все достаточно просто.
148
149 ```bash
150 #!/bin/bash
151
152 ME=`whoami`
153 sudo ./snapshooter.pl init mnt image.img 40000000 10
154 sudo chown $ME: mnt/_reference
155 chmod 700 mnt/_reference
156
157 cp -r etalon/* mnt/_reference
158
159 ~/crushers/latest/bin_x86-64/fuzz_manager --bitmap-size 300000 --start 4 --eat-cores 1 --dse-cores 0 -i in -o out -T StdIn -F -I StaticNoForkSrv --configurator-script conf.py --environment-plugin env.py -- /home/nataraj/tests/fuzz_psql/pg/bin/postgres --single postgres
160 ``` 
161
162 Таким образом удалось успешно запустить простейший параллельный фаззинг SQL-запросов с восстановлением состояния хранилища к исходному перед каждым запуском. На сам по себе прямой фаззинг SQL-запросов я больших надежд не возлагаю, фаззить в лоб сложные синтаксические конструкции -- малопродуктивно, однако система восстановления контекста в дальнейшем будет применена для фаззинг-исследования функциональности  потенциально приводящей к модификации хранилища postgresql. 
163
164 ## Приложения
165 ### snapshooter.pl
166 ```perl
167 #!/usr/bin/perl
168
169 use strict;
170
171 my $command = shift @ARGV;
172
173 die "Укажите команду первым аргументом" unless $command;
174
175 if ($command eq "init")
176 {
177     my $mount_path = shift @ARGV;
178     my $image_path = shift @ARGV;
179     my $snapshot_size = shift @ARGV;
180     my $snapshots_count = shift @ARGV;
181
182     unless ($image_path && $mount_path && $snapshot_size && $snapshots_count)
183     {
184       die "Команде init нужны 4 параметра: точка монтирования,путь к образу,  размер снэпшота и из кол-во";
185     }
186
187     `mountpoint -q $mount_path`;
188     unless( $?)
189     {
190         print "$mount_path -- примонтирован. Пробуем отмонтировать...\n";
191         `umount $mount_path`;
192     }
193
194     my $min_btrfs_size = 114294784;
195     if ($snapshot_size * $snapshots_count < $min_btrfs_size)
196     {
197         print "Увеличиваем размер образа до минимально возможных $min_btrfs_size байт";
198         $snapshot_size = $min_btrfs_size;
199         $snapshots_count = 1;
200     }
201
202     print "Создаем и форматируем образ...\n";
203     `dd if=/dev/zero of=$image_path count=$snapshots_count bs=$snapshot_size`;
204     `mkfs.btrfs $image_path`;
205
206     print "Монтируем образ...\n";
207     `mkdir -p $mount_path`;
208     `mount $image_path $mount_path`;
209
210     print "Создаем reference снэпшот...\n";
211     `btrfs subvolume create $mount_path/_reference`;
212 }
213
214 if ($command eq "clone")
215 {
216     my $mount_path = shift @ARGV;
217     my $target = shift @ARGV;
218
219     unless ($target)
220     {
221       die "Команде $command нужны 2 параметра: точка монтирования, имя снэпшота";
222     }
223
224     my $name = "$mount_path/$target.base";
225
226     if (-e $name)
227     {
228         die "Снэпшот $name уже существует";
229     }
230     print "Создаю основу для снэпшота $target: $name\n";
231
232     `btrfs subvolume snapshot $mount_path/_reference $name`;
233 }
234
235 if ($command eq "reset")
236 {
237
238     my $mount_path = shift @ARGV;
239     my $target = shift @ARGV;
240
241     unless ($target)
242     {
243       die "Команде $command нужны 2 параметра: точка монтирования, имя снэпшота";
244     }
245     my $name = "$mount_path/$target";
246     unless (-e "$name.base")
247     {
248         die "Не существует основы для снэпшота $target: $name.base";
249     }
250
251     print "Обновляю снэпшот $target: \n";
252
253     if (-e $name)
254     {
255         `rm -r $name`;
256          print "Удаляем старый\n";
257     }
258
259     print "Создаем новый\n";
260
261     `btrfs subvolume snapshot $name.base $name`;
262 }
263 ```
264