Important Notice: this service will be discontinued by the end of 2024 because for multiple years now, Plume is no longer under active/continuous development. Sadly each time there was hope, active development came to a stop again. Please consider using our Writefreely instance instead.

Подготовка окружения для исследуемого процесса с испольнованием btrfs

Часто при фаззинге сложных проектов может оказаться, что для нормальной работы каждому экземпляру исследуемой программы нужна собственная директория с данными, которую не плохо бы перед каждым новым прогоном исследуемой программы возвращать в исходное состояние. В данном посте в формате "заметок" я расскажу как мне удалось сделать восстановление окружения при фаззинге postgres'а, используя btrfs под фаззером Crusher.

Для фаззига 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`;
}