Как Эльбрус помог исправить уязвимость в tar

Начало истории: https://blog.handydev.com/admin/edit-content/compiling-apps-in-protected-mode

Эльбрус в защищённом режиме всё-таки выявил настоящую уязвимость в программе tar c доступом в неинициализированную область памяти! Поиск и исправление занял не очень много времени, т.к. сбойнул лишь один тест, а программа компактная.

Не проходил тест, добавленный этим комитом: https://git.savannah.gnu.org/cgit/tar.git/commit/?id=336519aa4f81415a34064d3342ce1d984be5f290

Но напрямую проблема не была связана с изменениями, связанными с ним; однако, вызывалась уязвимость именно файлом архива нулевого размера, открываемым в процессе работы с некорректным файлом, на самом деле не являющимся архивом tar.

Как несложно убедиться в файле buffer.c, метод gnu_flush_read читает входной файл блоками размером с record_size, но не переживает, если прочитано на самом деле меньше. Ну а далее, разумеется, все эти 10240 байт случайной памяти попадают на вход других методов, понятно, что это плохо.

Исправление было несложным.

if(status < record_size) 
        {
          //We didn't read the entire buffer, so we need to fill it with zeroes
          size_t unreadSize = record_size - status;
          memset(record_start->buffer + status, 0, unreadSize);
        }

После строки 1522 закрывают уязвимость. Теперь пропатченный tar проходит все тесты в защищённом режиме!

## ------------- ##
## Test results. ##
## ------------- ##

219 tests were successful.
19 tests were skipped.

Патч отправлен: https://savannah.gnu.org/patch/index.php?10081

Компиляция приложений Эльбрус в защищённом режиме

Это пост о первых экспериментах. Теоретическая цель состоит в построении рабочего окружения, защищённого от уязвимостей переполнения буфера и т.п.

Многие опробованные программы "с лёту" скомпилировать в защищённом режиме не удалось, т.к. они имеют зависимости от 64-битных библиотек, т.е., нужно по цепочке компилировать всё окружение.

Из полезных приложений, легко оказалось скомпилировать tar:

./configure CFLAGS='-m128' LDFLAGS='-m128'
make

Большая часть тестов выполнилась успешно, один почему-то сбойнул - а в обычном режиме отработали хорошо.

188: updating short archives                         FAILED (shortupd.at:34)

Кажется, тут стоит копнуть поглубже - возможно, защищённый режим выявил баги в tar.

Аналогично без проблем скомпилировался и редактор ed, который, впрочем, мало кому уже нужен.

Интересно, что tar в -m128 получился 2.6 Мб, а в обычном режиме - 3.8 Мб.

Скорость работы защищённого режима Эльбрус. Наивный тест.

Начнём с наивного сравнения скорости заполнения массива целых чисел размером в 1 миллион ячеек.

Так как защищённый режим влияет на работу с памятью, логично тестировать в первую очередь обращения к ней.

Для удобства дальнейшей работы определим макрос для проверки текущего режима выполнения программы.

protected_utils.h

#define E2K_PROTECTED_MODE sizeof(void*) == 16

protected_benchmark.c

#include <stdio.h>
#include <time.h>
#include <sys/time.h>
#include <stdint.h>
#include "protected_utils.h"

void main() {
    int sizeOfArray = 1000000;
    int numberOfRuns = 50;

    unsigned long accumulatedDuration = 0;

    for(int retry = 0; retry < numberOfRuns; retry++) {
        struct timeval timestampBegin;
        gettimeofday(&timestampBegin, NULL);

        int x[sizeOfArray];
        for(int i = 0; i < sizeOfArray; i++ ) {
            x[i] = i;
        }

        struct timeval timestampEnd;
        gettimeofday(&timestampEnd, NULL);

        unsigned long timeInterval = 1000000 * (timestampEnd.tv_sec - timestampBegin.tv_sec) +
        timestampEnd.tv_usec - timestampBegin.tv_usec;
        accumulatedDuration += timeInterval;
    }

    printf("Array assignment benchmark executed in: %lu\n", accumulatedDuration / (unsigned long)numberOfRuns);
    if(E2K_PROTECTED_MODE) {
        printf("Benchmark executed in protected mode.\n");
    } else {
        printf("Benchmark executed in non-protected mode.\n");
    }
}

Обычный режим:

Array assignment benchmark executed in: 10071
Benchmark executed in non-protected mode.

Что интересно, в обычном режиме многократный запуск теста практически не влияет на результаты, разброс в пределах 0.6%

Защищённый режим:

Array assignment benchmark executed in: 19899
Benchmark executed in protected mode.

Однако, многократный запуск даёт разброс вплоть до 23057, что составляет разницу в целых 15%.

Если сравнивать минимальные результаты, то на таком наивном тесте без оптимизаций защищенный режим медленнее обычного на 98%.

Скомпилируем наш пример с оптимизацией О3.

Обычный режим.

Array assignment benchmark executed in: 507
Benchmark executed in non-protected mode.

Защищённый режим.

Array assignment benchmark executed in: 5322
Benchmark executed in protected mode.

При этом в защищённом режиме практически пропал разброс результатов между запусками, но он стал на 950% медленнее обычного.

Пока сложно сказать, что именно вызывает такую разницу - плохие оптимизации lcc в режиме m128 или физические ограничения защищённого режима на уровне процессора.

Защищённый режим Эльбрус. Введение.

Здесь и далее работаем на E8C-SWTX с 4 восьмиядерными процессорами.

Для понимания того, что такое защищённый режим, нужно обязательно прочитать: http://ftp.altlinux.org/pub/people/mike/elbrus/docs/elbrus_prog/html/chapter11.html

Поэкспериментируем с возможностями защищённого режима.

Сначала - простейший пример.

#include <stdio.h>

void main() {
        int testArray[14];
        testArray[15] = 0;
        printf("We should have crashed before!\n");
}

В обычном режиме:

We should have crashed before!

(То есть, здесь и далее - мы успешно вылезли за законную выделенную память). В защищённом режиме:

Ошибка сегментирования

Разумеется, эту ошибку мог отловить и статический анализатор кода.

Усложним пример, чтобы убедиться, что компилятору ничего не известно о размере массива на этапе сборки.

#include <stdio.h>
#include <stdlib.h>

void main() {
        int arraySize;
        printf("Enter array size\n");
        scanf("%d", &arraySize);
        int *testArray = (int*)malloc(arraySize * sizeof(int));
        testArray[arraySize] = 0;
        printf("We should have crashed before!\n");
}

В обычном режиме мы снова попадаем туда, куда не должны:

Enter array size
10
We should have crashed before!

В защищённом режиме:

Enter array size
10
Ошибка сегментирования

Поэкспериментируем с неинициализированными данными.

#include <stdio.h>
#include <stdlib.h>

void main() {
        int arraySize;
        printf("Enter array size\n");
        scanf("%d", &arraySize);
        int *testArray = (int*)malloc(arraySize * sizeof(int));
        printf("We shouldn't output this uninitalized data: %d\n", testArray[0]);
}

В обычном режиме мы можем читать неинициализированные данные:

Enter array size
10
We shouldn't output this uninitalized data: 0

В защищённом режиме:

Enter array size
10
Недопустимая инструкция

Функции защищённого режиме поддерживаются как в С, так и в С++.

Проверить выполняемый файл на предмет: обычный он или защищённый, легко командой ldd

Обычное приложение:

libc.so.6 => /lib64/libc.so.6 (0x000046382dade000)
/lib64/ld-linux.so.2 (0x000046382da2b000)

Защищённое приложение:

libc.so.6 => /lib128/libc.so.6 (0x0000000050ea5000)
/lib128/ld-linux.so.2 (0x0000000050180000)

Различие в размере для простейшей программы (пример 3) без оптимизации:

7992/16336 байт (обычный/защищённый режим).

С оптимизацией O3:

7984/16336 байт.

В следующей части будем разбираться, насколько защищённый режим влияет на скорость работы.