Тест date-debug не срабатывает в ЗР из-за какой-то проблемы внутри gnulib:
./date --debug -d 'TZ="America/Edmonton" 2006-04-02 02:30:00'
date: parsed date part: (Y-M-D) 2006-04-02
date: parsed time part: 02:30:00
date: input timezone: TZ="America/Edmonton" in date string
date: using specified time as starting value: '02:30:00'
date: error: invalid date/time value:
Недопустимая инструкция
Оставим эту проблему на потом.
Программа pr валится с недопустимой инструкцией на:
pr --date-format="-- Date/Time --" -h x -b -3 './tests/pr/0Ft'
По поводу патча для randread.c мне сказали, что в курсе о использовании неинициализированной памяти, и это нормально. Поэтому патч пришлось переделать на e2k-специфический:
diff --git a/gl/lib/randread.c b/gl/lib/randread.c
index 8e6b1c5b8..ccad16bc3 100644
--- a/gl/lib/randread.c
+++ b/gl/lib/randread.c
@@ -132,7 +132,13 @@ static struct randread_source *
simple_new (FILE *source, void const *handler_arg)
{
struct randread_source *s = xmalloc (sizeof *s);
+ #ifdef __e2k__
+ if (sizeof(void*) == 16)
+ {
+ // In E2K protected mode uninitialized memory can't be used
+ memset(s, 0, sizeof *s);
+ }
+ #endif
s->source = source;
s->handler = randread_error;
s->handler_arg = handler_arg;
Скомпилировать coreutils в защищённом режиме несложно:
./configure CFLAGS='-Wno-error=suggest-attribute=const -m128 -Wno-error=suggest-attribute=pure -Wno-error=suggest-attribute=noreturn -Wno-error=suggest-attribute=format -Wno-error=suggest-attribute=cold -Wno-error=suggest-attribute=malloc -Wno-error=bad-macro-redef -Wno-error=type-limits -Wno-error=shadow=global' LDFLAGS='-m128'
Изначально не проходит достаточно много тестов:
FAIL: tests/misc/help-version.sh
FAIL: tests/pr/pr-tests.pl
FAIL: tests/misc/date-debug.sh
FAIL: tests/misc/sort.pl
FAIL: tests/misc/sort-discrim.sh
FAIL: tests/misc/sort-month.sh
FAIL: tests/misc/sort-rand.sh
FAIL: tests/misc/yes.sh
FAIL: tests/du/long-from-unreadable.sh
FAIL: tests/rmdir/symlink-errors.sh
В тесте help-version.sh на самом деле вся проблема в программе shuf - она попросту не запускается.
Проблема - в создании неинициализированного блока памяти в lib/randread.c
static struct randread_source *
simple_new (FILE *source, void const *handler_arg)
{
struct randread_source *s = xmalloc (sizeof *s);
s->source = source;
s->handler = randread_error;
s->handler_arg = handler_arg;
return s;
}
(дальше s->buf используется в lib/rand-isaac.c, ISAAC_MIX как переменная seed):
/* The basic ISAAC initialization pass. */
#define ISAAC_MIX(s, a, b, c, d, e, f, g, h, seed) \
{ \
int i; \
\
for (i = 0; i < ISAAC_WORDS; i += 8) \
{ \
a += seed[i]; \
b += seed[i + 1]; \
c += seed[i + 2]; \
d += seed[i + 3]; \
e += seed[i + 4]; \
f += seed[i + 5]; \
g += seed[i + 6]; \
h += seed[i + 7]; \
mix (a, b, c, d, e, f, g, h); \
s->m[i] = a; \
s->m[i + 1] = b; \
s->m[i + 2] = c; \
s->m[i + 3] = d; \
s->m[i + 4] = e; \
s->m[i + 5] = f; \
s->m[i + 6] = g; \
s->m[i + 7] = h; \
} \
}
Решение очевидно и корректно - очищаем память после создания.
static struct randread_source *
simple_new (FILE *source, void const *handler_arg)
{
struct randread_source *s = xmalloc (sizeof *s);
memset(s, 0, sizeof *s);
s->source = source;
s->handler = randread_error;
s->handler_arg = handler_arg;
return s;
}
Программа shuf теперь работает. Новый список невыполненных тестов стал существенно короче:
FAIL: tests/pr/pr-tests.pl
FAIL: tests/misc/date-debug.sh
FAIL: tests/misc/tac.pl
FAIL: tests/misc/yes.sh
FAIL: tests/du/long-from-unreadable.sh
FAIL: tests/rmdir/symlink-errors.sh
Следующей исправим программу уes
Она полагается на весьма вольное допущение о том, что элементы, переданные в argv, следуют одним блоком памяти, один за другим (хотя на самом деле это массив указателей на указатели и не более того). Но проверяют это в строчках
do
{
size_t operand_len = strlen (*operandp);
bufalloc += operand_len + 1;
if (operandp + 1 < operand_lim
&& *operandp + operand_len + 1 != operandp[1])
reuse_operand_strings = false;
}
while (++operandp < operand_lim);
Тут (в обычном режиме), если указатели расположены не единым блоком, то данная оптимизация отключается. В защищённом режиме это всё не работает, потому что за границы указателя выходить нельзя ни при каких условиях.
Добавляем специальный код для защищённого режима E2K
#ifdef __e2k__
if (sizeof(void*) == 16)
{ // in protected mode, pointers are 128-bit
reuse_operand_strings = false;
}
#endif
Новый список невыполненных тестов:
FAIL: tests/pr/pr-tests.pl
FAIL: tests/misc/date-debug.sh
FAIL: tests/misc/tac.pl
FAIL: tests/misc/tac-2-nonseekable.sh
FAIL: tests/du/long-from-unreadable.sh
FAIL: tests/rmdir/symlink-errors.sh
Теперь появился новый сбойнувший тест tac, который, видимо, зависел от работы yes.
Начало истории: 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(×tampBegin, NULL);
int x[sizeOfArray];
for(int i = 0; i < sizeOfArray; i++ ) {
x[i] = i;
}
struct timeval timestampEnd;
gettimeofday(×tampEnd, 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 байт.
В следующей части будем разбираться, насколько защищённый режим влияет на скорость работы.