11. Режим Безопасных Вычислений

В этом дополнении будет кратко описан особый режим компиляции и исполнения программ на C/C++, называемый Режимом Безопасных Вычислений (РБВ).

11.1. Назначение и общий принцип работы РБВ

Идея использования РБВ заключается во введении дополнительного контроля поведения программ на С/С++, отсутствующего в обычных реализациях этих языков. Контроль опирается на специализированные аппаратные функции платформы Эльбрус. Его ключевые особенности:

  • контролируются обращения за границами объектов,

  • контролируется использование неинициализированных переменных,

  • контролируется создание новых указателей и конверсия указателей.

Дополнительный контроль позволяет обнаруживать сложные ошибки исполнения программ, связанные с переполнением буфера, использованием случайных данных. Более того, в РБВ становится невозможной эксплуатация многих программных уязвимостей, которые не были обнаружены до начала эксплуатации.

Для работы программы в РБВ вводятся дополнительные требования к её исходным текстам. Они более жёсткие, чем общие стандарты языков C/C++.

РБВ основан на аппаратной поддержке контроля ссылок/указателей, типов данных и контекста исполнения, с точностью до модуля. Аппаратный контроль (технология безопасных вычислений, ТБВ) является более глубоким и общим свойством архитектуры Эльбрус, и может быть задействован в реализации разных языков программирования. На данный момент времени он используется только для языков C/C++, и в руководстве мы ограничимся его описанием в данной части.

11.2. Устройство указателя в РБВ

В режиме РБВ указатели расширяются до дескрипторов. В отличие от обычного указателя C/C++, который совместим с целочисленным типом, дескриптор РБВ содержит в себе дополнительную информацию:

  • указатель на начало выделенной области памяти/объекта (64 бита);

  • размер выделенной памяти/объекта (32 бита);

  • смещение относительно начала (32 бита).

Дескриптор в ТБВ имеет размер 128 бит, выровнен по размеру и подтверждён аппаратными тэгами. В рамках ТБВ его можно получить ограниченным числом способов:

  • получить от ядра операционной системы с помощью системного вызова (malloc);

  • получить указатель на процедурный фрейм пользовательского стека;

  • сконверировать из другого указателя, с уменьшением области памяти (переход от объекта к подобъекту);

  • скопировать из указателя на область глобалов модуля.

11.3. Тэги

Аппаратные тэги - добавочные биты информации, которые хранятся отдельно от основных данных и используются для разметки дополнительных свойств ТБВ. Для размещения тэгов «рядом» с данными в оперативной памяти используется часть битов ECC.

Тэги позволяют сохранять в процессе исполнения информацию о типах данных и в дальнейшем контролировать их аппаратно. Для РБВ используются типы:

  • числовые данные;

  • указатель (дескриптор);

  • неинизиализированные данные.

Наличие тэгов учитывается семантикой всех операций из системы команд, но в руководстве мы ограничимся лишь самыми общими правилами:

  • целостность дескриптора сохраняется лишь в том случае, когда дескриптор записывается целиком (атомарно), в пару соседних регистров (квадрорегистр) либо в выровненные 16 байт памяти;

  • операции чтения и записи проверяют целостность тэгов аргумента-дескриптора, и вырабатывают исключение/диагностиику, если целостность тэга нарушена.

Таким образом, с помощью тэгов гарантируется одно из главных свойств РБВ:

  • работоспособный дескриптор нельзя получить из частей разных дескрипторов и/или числовых данных, ни случайно, ни специально.

11.4. Дополнительные ограничения на исходные коды

Для компиляции в РБВ программа на C/C++:

  • не должна содержать преобразование из целого в указатель;

  • не должна закладываться на sizeof(void*) == sizeof(long);

  • не должна закладываться на sizeof(void*) == sizeof(double);

  • не может содержать отдельных объектов размером более 2^32 - это ограничение является временным для текущей реализации.

В процессе исполнения в РБВ программа получит исключительную ситуацию:

  • при обращении по указателю за пределами, указанными в дескрипторе [base, base+size];

  • при использовани данных, прочтенных из неинициализированной памяти;

  • при попытке разыменования некорректного дескриптора (попытка неявного преобразования целого в указатель через память).

Полный список ограничений достаточно велик и выходит за рамки документа. Заметим, что довольно часто нарушения дополнительных ограничений происходит в собственных менеджерах памяти программ.

11.5. Частичные аналоги РБВ

Идеи контроля границ объектов были реализованы в отечественной и мировой практике как программными средствами (valgrind, address sanitizer = asan), так и на уровне аппаратуры (cheri). В некоторых ситуациях РБВ работает точнее своих аналогов, обнаруживая нарушения границ на любое смещение, в отличие от, например, asan.

11.6. Пример сборки и исполнения теста

#include <stdlib.h>
#define N 1000

int *gp;
int main()
{
  gp = (int*)malloc(N*sizeof(int));
  gp[N/2] = N/2;
  gp[-1] = 14; // ошибка
  return 0;
}
$ /opt/mcst/bin/lcc ./t.c -o t
$ ./t
$ /opt/mcst/bin/lcc -mptr128 ./t.c -o t_128
$ ./t_128
Segmentation fault

Как видно из примера, версия теста в РБВ упала на исполнении с ошибкой сегментации, в то время как в обычном режиме тест отработал без ошибки, несмотря на запись за пределами выделенного объекта.

11.7. Ассемблер теста в РБВ

Посмотрим на ассемблер функции main теста из предыдущего раздела.

main:
      {
        nop 1
        setwd wsz = 0x13, nfx = 0x1, dbl = 0x0
        setbn rsz = 0x7, rbs = 0xb, rcur = 0x0
        setbp psz = 0x0
        getsap,0      _f32s,_lts1 0xffffffe0, %r18
      }
      {
        nop 1
        stapb,2       %r18, 0x0, %r18
      }
      {
        disp  %ctpr1, malloc; ipd 2
        movtq,0,sm    %r18, %b[0]
        addd,2        0x0, _f16s,_lts0lo 0xfa0, %b[2]
        adds,3        0x0, _f16s,_lts0hi 0x1f4, %r1
        adds,4        0xe, 0x0, %r2
      }
      {
        nop 3
        aptoapb,0     %r18, _f16s,_lts0lo 0x20, %b[0]
      }
      {
        call  %ctpr1, wbs = 0xb
      }
      {
        return        %ctpr3; ipd 2
        stgdq,2,sm    0x0, [ _f32s,_lts0 gp ], %b[0]
        adds,3        0x0, 0x0, %r0
      }
      {
        stapw,5       %b[0], _f16s,_lts0lo 0x7d0, %r1
      }
      {
        nop 2
        ldgdq,0       0x0, [ _f32s,_lts0 gp ], %r20
      }
      {
        stapw,2       %r20, _f16s,_lts0lo 0xfffc, %r2
      }
      {
        ct    %ctpr3
      }

Отличия от обычного режима исполнения:

  • получение указателя на фрейм стека: getsap вместо getsp; результатом является 128-битный дескриптор.

  • команда stapw вместо stw: обращение в память по дескриптору. В качестве аргумента должен быть 128-битный регистр, реализованный в качестве пары соседних 64-битных регистров, в котором находится дескриптор.

  • movtq копирует 128-битный дескриптор с сохранением тэгов.

  • команда преобразования указателя aptoapb: возвращает дескриптор, полученный из дескриптора %r18-%r19 в первом аргументе, до размера 0x20, указанного во втором аргументе.

  • команды stgd и ldgd нужны для обращения к глобальным объектам. В качестве дескриптора области глобальных объектов выступает регистр %gd.