Для фаззига postgres’а система восстановления контекста чрезвычайно важна. Полноценный postgres не может работать без своего личного хранилища, и при этом меняет его практически на каждый чих. Без возвращения хранилища к исходному состоянию при тестировании, ни о какой воспроизводимости и речи быть не может. Поэтому как только заходит речь о фаззинг-тестировании postgres’а в сборе, необходимо сразу думать о системе восстановления контекста.
btrfs – файловая система со снепшотами
В качестве framework’а для системы восстановления контекста была выбрана файловая система btrfs. Btrfs позволяет эффективно работать со снепшотами на уровне директорий. А именно, позволяет в рамках файловой системы получить клон указанной директории с минимальными затратами. Клонирование происходит на логическом уровне, данные по факту не копируются, и только лишь изменения внесенные в копию или в оригинал занимают дополнительное место на диске. Все это позволяет экономить время и дисковое пространство.
Фаззинг, при его промышленном применении, имеет практический смысл, будучи запущенным во много потоков на многих процессорах. Для каждого инстанса фаззера, а точнее для исследуемой программы запущенной этим фаззером, предполагается создать свой собственный снепшот с уникальным именем, содержащий данные необходимые для запуска исследуемой программы, и возвращать этот снепшот к исходному состоянию перед каждым новым прогоном исследуемой программы, чтобы следы оставленные в хранилище предыдущими прогонами не оказывали влияния на последующее..
Мной был написан скрипт snapshooter.pl
(код приведен в конце поста) реализующий действия со снепшотами, необходимые для фаззинга. Скрипту первым параметром передается имя команды и далее параметры которые нужны команде для работы.
Ниже приведены команды и их описания:
init MOUNT_POINT IMAGE_FILE IMAGE_SIZE IMAGE_COUNT
Принимает 4 аргумента: точка монтирования MOUNT_POINT
, путь к создаваемому образу файловой системы IMAGE_FILE
, предполагаемый размер одного снепшота IMAGE_SIZE
и ожидаемое количество снепшотов IMAGE_COUNT
.
Скрипт отмонтирует то, что было примантировано к точки монтирования, создаст новый образ файловой системы, примонтирует его к точке монтирования, и создаст в файловой системе директорию _reference
.
В эту директорию следует скопировать “эталонный” снепшот, на базе которого будут строиться снепшоты для всех экземпляров
clone MOUNT_POINT SNAPSHOT_NAME
Принимает два аргумента: точку монтирования IMAGE_COUNT
и имя снэпшота SNAPSHOT_NAME
.
Команда clone
клонирует эталонный снепшот в директорию [SNAPSHOT_NAME].base
. Если есть потребность внести какие-то специфичные для инстанса изменения в снепшот, то это следует сделать в этой директории. В дальнейшем перед каждым запуском исследуемой программы рабочий снепшот будет приводится к “базововму” состоянию.
reset MOUNT_POINT SNAPSHOT_NAME
Принимает два аргумента: точку монтирования MOUNT_POINT
и имя снэпшота SNAPSHOT_NAME
.
Команда reset
удаляет снепшот SNAPSHOT_NAME
, и создает новый с таким же именем клонируя базовый снепшот [SNAPSHOT_NAME].base
. Таким образом происходит восстановление целевого снепшота исходное состояние.
В дальнейшем, для ускорения работы, планируется образ файловой системы со снепшотами перенести с диска в память, но я пока еще не разбирался как это делается.
Crusher
В Crusher 2.11.0 появились инструменты позволяющие инициировать подготовку, и восстановления окружения для запускаемых процессов. К сожалению для того чтобы задействовать эти инструменты необходимо написание скриптов на языке python (и ни на каком другом), а я этот python не умею, и честно говоря не очень хочу уметь. К счастью коллеги снабдили меня обертками которые позволяют из непонятного питона запускать понятные .sh
и прочие скрипты. Поэтому конструкция получилась двухсоставная.
Предложенное решение использует два “хука” предоставляемые крашером. Первый хук дергается при создании нового инстанса фаззера (см. --configurator-script
в документации), и используется для создания нового снепшота с данными хранилища, которыми будет пользоваться исследуемая программа. Второй хук дергается перед каждым новым прогоном исследуемой программы (см. --environment-plugin
в документации), и используется для того чтобы вернуть снепшот с хранилищем в исходное состояние (ибо есть шанс что при предыдущих запусках он мог быть иземенен)
Подготовка окружения
Изначальная подготовка окружения осуществляется через скрипт conf.py
:
import json
import os
import sys
import traceback
import subprocess
def transform_options(ops_json):
try:
jops = json.loads(ops_json)
instance_name = jops['configuration']['instance_name']
this_dir = os.path.dirname(__file__)
script_path = this_dir + '/init_script.sh'
pr = subprocess.Popen([script_path, instance_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
pr.wait()
assert pr.returncode == 0
output = pr.stdout.read()
for line in output.split():
ls = line.split('=')
assert len(ls) == 2
var_name = ls[0].strip()
var_value = ls[1].strip()
os.environ[var_name] = var_value
return json.dumps(jops)
except Exception as ex:
print("EXCEPTION!")
traceback.print_exc()
return None
Который в свою очередь вызывает скрипт init_script.sh
:
#!/bin/bash
INSTANCE_NAME=$1
THIS_DIR=$(dirname $0)
sudo ./snapshooter.pl clone mnt "${INSTANCE_NAME}" 1>&2 || exit 1
sudo ./snapshooter.pl reset mnt "${INSTANCE_NAME}" 1>&2 || exit 1
echo "PGDATA=${THIS_DIR}/mnt/$INSTANCE_NAME"
Скрипту init_script.sh
в качестве аргумента передается уникальное имя инстанса фаззера, это имя скрипт использует в качестве имени создаваемого снепшота. Кроме того на STDOUT
скрипт выводит пары NAME=VALUE
переменных окружения которые должны быть установлены при запуске. В нашем случае мы через переменную окружения сообщаем постгресу где находится его хранилище. Вывод скрипта анализируется оберткой env.py
и значения переменных окружения передаются фаззеру для последующего применения.
При запуске фаззера необходимо указать расположение скрипта conf.py
через параметр --configurator-script
Восстановление окружения
Идея та же, что и с созданием окружения. Питоновская обертка env.py
:
import json
import os
error_msg = '...'
def get_error():
return error_msg
instance_name = None
def init(json_options):
jops = json.loads(json_options)
global instance_name
instance_name = jops['configuration']['instance_name']
return True
def finish():
return True
def setup():
this_dir = os.path.dirname(__file__)
script_path = this_dir + '/run_script.sh'
cmd = script_path + ' ' + instance_name
r = os.system(cmd)
return r == 0
def teardown():
return True
И .sh
скрипт выполняющий содержательную работу:
#!/bin/bash
INSTANCE_NAME=$1
sudo ./snapshooter.pl reset mnt "${INSTANCE_NAME}" 1>&2 || exit 1
В данном случае мы получаем имя инстанса в качестве аргумента и восстанавливаем снепшот с этим именем в исходное состояние.
При запуске фаззинга расположение файла env.py
указывается через параметр конфигурации --environment-plugin
Запуск фаззинга
Перед запуском фаззинга мы должны инициализировать хранилище снепшотов, раздать эталонному снепшоту правильные права (постгрес требует чтобы хранилище не было доступно на чтение кому либо кроме постгреса) и заполнить правильными данными. Заранее подготовленный снимок хранилища у меня лежит в директории etalon
, я его просто копирую. Ну а дальше все достаточно просто.
#!/bin/bash
ME=`whoami`
sudo ./snapshooter.pl init mnt image.img 40000000 10
sudo chown $ME: mnt/_reference
chmod 700 mnt/_reference
cp -r etalon/* mnt/_reference
~/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
Таким образом удалось успешно запустить простейший параллельный фаззинг SQL-запросов с восстановлением состояния хранилища к исходному перед каждым запуском. На сам по себе прямой фаззинг SQL-запросов я больших надежд не возлагаю, фаззить в лоб сложные синтаксические конструкции – малопродуктивно, однако система восстановления контекста в дальнейшем будет применена для фаззинг-исследования функциональности потенциально приводящей к модификации хранилища postgresql.
Приложения
snapshooter.pl
#!/usr/bin/perl
use strict;
my $command = shift @ARGV;
die "Укажите команду первым аргументом" unless $command;
if ($command eq "init")
{
my $mount_path = shift @ARGV;
my $image_path = shift @ARGV;
my $snapshot_size = shift @ARGV;
my $snapshots_count = shift @ARGV;
unless ($image_path && $mount_path && $snapshot_size && $snapshots_count)
{
die "Команде init нужны 4 параметра: точка монтирования,путь к образу, размер снэпшота и из кол-во";
}
`mountpoint -q $mount_path`;
unless( $?)
{
print "$mount_path -- примонтирован. Пробуем отмонтировать...\n";
`umount $mount_path`;
}
my $min_btrfs_size = 114294784;
if ($snapshot_size * $snapshots_count < $min_btrfs_size)
{
print "Увеличиваем размер образа до минимально возможных $min_btrfs_size байт";
$snapshot_size = $min_btrfs_size;
$snapshots_count = 1;
}
print "Создаем и форматируем образ...\n";
`dd if=/dev/zero of=$image_path count=$snapshots_count bs=$snapshot_size`;
`mkfs.btrfs $image_path`;
print "Монтируем образ...\n";
`mkdir -p $mount_path`;
`mount $image_path $mount_path`;
print "Создаем reference снэпшот...\n";
`btrfs subvolume create $mount_path/_reference`;
}
if ($command eq "clone")
{
my $mount_path = shift @ARGV;
my $target = shift @ARGV;
unless ($target)
{
die "Команде $command нужны 2 параметра: точка монтирования, имя снэпшота";
}
my $name = "$mount_path/$target.base";
if (-e $name)
{
die "Снэпшот $name уже существует";
}
print "Создаю основу для снэпшота $target: $name\n";
`btrfs subvolume snapshot $mount_path/_reference $name`;
}
if ($command eq "reset")
{
my $mount_path = shift @ARGV;
my $target = shift @ARGV;
unless ($target)
{
die "Команде $command нужны 2 параметра: точка монтирования, имя снэпшота";
}
my $name = "$mount_path/$target";
unless (-e "$name.base")
{
die "Не существует основы для снэпшота $target: $name.base";
}
print "Обновляю снэпшот $target: \n";
if (-e $name)
{
`rm -r $name`;
print "Удаляем старый\n";
}
print "Создаем новый\n";
`btrfs subvolume snapshot $name.base $name`;
}
Comments
No comments yet. Be the first to react!