Статьи и публикации

TrustZone: аппаратная реализация в ARMv7A

Интернет-портал Habrahabr.ru, октябрь, 2017
Статья Андрея Волкова, руководитель направления доверенной платформы компании "Аладдин Р.Д."

Сегодня начинаем исследовать внутреннее устройство TrustZone (это торговая марка компании ARM).

Само название — коммерческое, его придумали маркетологи, чтобы сообщить всему миру о ключевом свойстве этой технологии. По их задумке, мы должны представить какое-то доверенное, защищённое, очень надёжное место. Например, дом, где мы, закрыв двери и включив свет, чувствуем себя уютно и в безопасности.

Поэтому я начну с того, что TrustZone — это никакое не "место" в процессоре. Её нельзя найти на чипе, как кеш или АЛУ. И доверенные программы, на самом деле, не исполняются в какой-то физически выделенной зоне процессора.

Даже если мы посмотрели бы в исходные коды ядра ARM, то не смогли бы чётко выделить TrustZone. Скорее, по аналогии с программами, TrustZone — это несколько модулей и набор патчей для почти всех остальных частей процессора.

В этой статье мы рассмотрим, как TrustZone реализуется на аппаратном уровне процессоров ARM Cortex-A (ARMv7A).

В ARMv8A будет примерно то же самое, а вот в ARMv7M все совсем по-другому. В угоду маркетингу, там тоже есть TrustZone, но другая.

Режим

Первый компонент TrustZone — это режим процессора. Он задаётся битом NS (Non-Secure) в регистре SCR (Secure Configuration Register). Если NS=1, мы в режиме Non-Secure, если NS=0, мы в доверенном, то есть Secure-режиме.


Регистр SCR у Cortex-A5

Независимо от NS, остаются на своих местах все привычные режимы работы процессора. Cамые ходовые из них:

  • User — режим исполнения команды приложений;
  • Supervisor — режим работы ядра ОС;
  • IRQ — режим при обработке прерываний.

Благодаря NS у нас появляются Secure User, Non-Secure User, Secure Supervisor, Non-Secure Supervisor и так далее.

Названия User/Supervisor, приведённые здесь, используются для всех 32-битных ARM, до ARMv7 включительно. В ARMv8 применяются другие обозначения: EL0/EL1 и PL0/PL1. Сути это не меняет.

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

Более того, оказывается, что в нельзя взять и поменять значение бита NS ни в одном из обычных режимов работы процессора — это запрещено. Для смены значения NS предусмотрен церемониал с заходом процессора в отдельный режим Secure Monitor, который не принадлежит однозначно ни к Secure, ни к Non-Secure. Но об этом мы поговорим в следующей статье.

Получается, что NS раздваивает процессор, создаёт два неравноправных режима работы: Secure и Non-Secure. В каждом режиме, впрочем, есть все, что нужно для исполнения ОС и программ, просто привилегии по доступу к части функций CPU и периферии отличаются.

Режим, а не зона!

Продолжаем снимать завесы.

Доверенный режим исполнения программ — тот, где NS=0, все!

Нет никакого дополнительного конвейера команд, АЛУ, отдельной памяти программ, — ничего такого, что можно представить, услышав название TrustZone. Нет никакой границы этой зоны, команды нарушителей не стремятся "переползти" в доверенную зону, как вирусы сквозь клеточную мембрану.

В общем случае конвейер исполнял команды недоверенной программы (NS=1), а потом (бац!) произошло прерывание, процессор перешёл в доверенный режим (NS=0) и тут же исполняет доверенный код.

На самом деле, технология TrustZone даёт нам инструментарий, позволяющий предпринять ряд мер (разделить память доверенных и недоверенных программ, разделить доступ к периферии) для создания надёжного барьера между Secure и Non-Secure. Но надёжность этого барьера будет зависеть и от качества, и полноты реализации доверенного ПО.

Конец снятия завес.

Сигнал NS

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

Как это представить? В общем случае, мы представляем, что периферия к CPU подключена шинами адреса, данных и управления. NS входит в состав сигналов управления для тех процессоров, где TrustZone реализована. Таким образом, от CPU к устройству идут не просто команды Read, Write, а Secure Read, NonSecure Read, Secure Write, NonSecure Write.

Cortex-A чуть чаще, чем всегда поставляется как System On Chip (SoC), поэтому все эти шины скрыты от нас внутри чипа. Однако ряд SoC позволяют вывести сигнал NS наружу, на случай подключения внешней периферии, поддерживающей безопасный режим.

Какая периферия поддерживает Secure/NonSecure доступ? Например, это контроллер прерываний GIC — в ARM это периферийное устройство в составе SoC. В Secure-режиме он позволяет настроить доставку некоторых прерываний в режим Secure FIQ и запретить изменять эту настройку ПО из NonSecure-режима.

Вот что происходит при работе CPU с GIC: при записи регистра GIC в режиме Secure от CPU вместе с адресом регистра и данными идёт сигнал NS=0. GIC понимает, что запись доверенная, и даёт полный доступ. Если же NS=1, GIC ограничивает доступ к части регистров, как на запись, так и на чтение.

Другие блоки процессора, поддерживающие сигнал NS: контроллеры памяти, часы реального времени (RTC), хранилище ключей, контроллер сброса и управления питанием. Заметим, что в ARMv7A поддержка TrustZone опциональна, и при создании SoC опция Secure Extensions (читаем: TrustZone) может быть отключена. При этом из чипа удаляются ненужные блоки и связи, в частности, отпадает необходимость в трассировке линии NS по всему чипу. При этом входы NS периферийных устройств подключаются к 0 (по крайней мере, мы можем так это представить). Топология чипа становится проще.

Многопроцессорность

Что происходит, когда SoC содержит несколько процессорных ядер? Каждое ядро (обычно именно ядро называется CPU в документации ARM) может работать в режимах Secure или Non-Secure. В любой момент времени может оказаться, что одни ядра — Secure, а другие — нет.

А ещё, ядра, не поддерживающие TrustZone, могут быть объединены с ядрами, поддерживающими её, в одном SoC.

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

В процессорах ARM все процессорные ядра, память и периферию соединяет внутренняя шина, которая называется AMBA (https://en.wikipedia.org/wiki/Advanced_Microcontroller_Bus_Architecture). Начиная примерно с ARMv4, в cоставе шины AMBA существует блок коммутации, он подключает блоки, называемые Bus Master, к различным Slave-устройствам.

Только реально крепкий орешек разберётся в деталях работы AXI и AMBA, а ведь для полной картины нужно добавить AHB, APB и учесть детали реализации в разных архитектурах. Но общая идея улавливается очень быстро.

Например, процессорное ядро (а точнее, D-cache и I-cache этого процессора) — это Bus Master, а какой-нибудь I2C контроллер — это Slave. Bus Master начинает транзакцию по шине, то есть чтение или запись. Slave — это тот блок, куда пишут или откуда читают. Отсюда, кстати, следует и сам набор мастеров: ядра процессора, контроллеры DMA и периферия со встроенным DMA (как, например, USB host).

Блок коммутации Master Slave мы рассмотрим подробнее. В ARMv7A он называется Interconnect и является элементом реализации AXI (Advanced eXtensible Interface). В ARM926 этот блок имел говорящее название Bus Matrix и был частью реализации интерфейса внутренней шины AHB (Advanced High-Perfomance Bus). По сути — это то же самое. У нас есть M×Master и N×Slave, и есть матрица коммутации, соединяющая первых со вторыми. В каждый момент времени каждый Master может быть подключён к одному Slave или отключён совсем. Но несколько Master могут быть одновременно активны, если подключены к разным устройствам.

В общем случае не все связи возможны. В частности, дизайнер системы может исключить ненужные связи — например, если нет причин для Ethernet-контроллера (Master), можно писать напрямую в I2C-контроллер (Slave).

Кроме того, некоторые устройства могут быть как Master, так и Slave. Например, USB Host, когда сохраняет данные через DMA в память — Master, а когда мы настраиваем его регистры— Slave.

При этом каждый Master является и источником сигнала NS, а Slave — реципиентом этого сигнала. AXI транслирует через Interconnect сигналы NS от Master к соответствующему Slave, и благодаря этому в SoC могут одновременно происходить как Secure-, так и NonSecure-транзакции.

Периферия

Теперь мы видим, как в ARM Cortex-A поддерживается одновременная работа на внутренней шине нескольких процессорных ядер и множества периферийных устройств, одновременно в режимах Secure и Non-Secure. Ещё немного усложним?

При создании SoC разработчик берет блоки от ARM, блоки от сторонних производителей и блоки собственной разработки, соединяет их в единую систему.

От ARM берутся, в том числе

  • ядра процессоров, например, Cortex-A, Cortex-M4, или мультипроцессорная система целиком, например, Cortex-A9 MPCore;
  • контроллер прерываний GIC, например, PL390;
  • контроллер кеша, например, L2C-310.

Все они имеют поддержку TrustZone и внутри себя разделяют доступ по NS на доверенный и недоверенный.

Например, контроллер кеша знает, какие линейки были сохранены в доверенном режиме, а какие — в недоверенном, и будет производить соответствующие транзакции по AXI для сброса данных в физическую память.

Далее, многие блоки процессора покупаются у сторонних (надёжных и известных) разработчиков, они одни и те же даже в процессорах разных производителей. Это, например, USB host, SDHC host. Другие блоки разработчик SoC использует во всех своих процессорах, почти не меняя. Это, например, Ethernet MAC, контроллеры I2C, UART, SPI.

Вот эти покупные и свои блоки могут не иметь поддержки TrustZone совсем. Это объяснимо — мы не можем представить, зачем нужно разделять доступ к UART между Secure и Non-Secure. Но вопрос интеграции таких устройств в TrustZone повисает в воздухе.

Вопросы интеграции этих устройств решается производителем SoC самостоятельно. Фактически, производитель должен решить две задачи:

  • для Bus Master без поддержки TrustZone подставить верный NS-бит;
  • для Bus Slave обеспечить настройку и проверку прав доступа.

Доступ Bus Master без поддержки TrustZone

Посмотрим, что это значит для Bus Master на примере с видеоконтроллером, берущим данные из памяти и передающим их прямо в HDMI.

Мы хотим обеспечить пресловутый DRM: зашифрованный видеопоток будет поступать из Linux в Secure OS, там расшифровываться и отображаться на экране. Расшифрованные данные будут размещаться в области памяти, доступной только для Secure Read/Write, чтение этой области из Linux (Non-Secure) даст ошибку доступа. Таким образом, мы не дадим Linux скопировать расшифрованный поток. Видеоадаптер с правом Secure-доступа будет беспрепятственно читать расшифрованные видеоданные и отображать на экран.

Чтобы видеоадаптер мог через AXI получать данные из Secure-памяти, он должен осуществлять доступ с NS=0. Однако если DRM нам противен не нужен, предоставлять видеоконтроллеру привилегированный доступ мы можем и не захотеть.

Чтобы контроллер работал так и этак, в системе вводится настройка: тип доступа для каждого Bus Master, не поддерживающего TrustZone. То есть минимум 1 бит на каждый Bus Master. Возможно, это просто один регистр — но это работа для создателя SoC, его ответственность. И это, конечно, источник несовместимости между процессорами разных производителей.

Доступ Bus Slave без поддержки TrustZone

Для каждого устройства Slave разумно будет определить следующие права доступа при работе с AXI:

  • разрешён ли доступ Secure Read;
  • разрешён ли доступ Secure Write;
  • разрешён ли доступ Non-Secure Read;
  • разрешён ли доступ Non-Secure Write.

Этот набор вытекает из суперпозиции операций Read/Write и режимов Secure/Non-Secure. На самом деле, как делить в данном случае права, решает производитель SoC самостоятельно. Например, можно уменьшить количество настроек, разрешив Secure-доступ всегда. А можно и увеличить, добавив разбиение по типам доступа User/Supervisor.

Для такого контроля доступа можно под каждый Bus Slave предусмотреть регистр с 2-4-8 битами, разрешающими или запрещающими доступ к устройству в зависимости от режима доступа.

И тут мы подошли к еще одной теме: а что будет, если Bus Master доступ начал, а Bus Slave его не разрешил?

Ошибка доступа

Если есть ограничение, то будет и нарушение. Если какой-то тип доступа к устройству запрещён, что-то должно произойти, если его осуществить.

На самом деле, не всегда. Например, в том же GIC (контроллер прерываний), запрещённые для Non-Secure операции записи не выполняются (тихо и спокойно), а операции чтения возвращают нули. Ничего не происходит, и это специально так задумано — позволяет запускать одну и ту же ОС (например, Linux) как в Secure-, так и в Non-Secure-режимах. В Secure-режиме Linux будет настраивать все самостоятельно, в Non-Secure — контроллер будет преднастроен, и Linux сможет настроить только то, что ей осталось разрешено. Но она и глазом не моргнет, не заметит подвоха, потому что GIC никакой ошибки при записи в запрещённую область не выдаст.

А что если мы используем менее хитрые интеллектуальные устройства? Тогда, например, при Non-Secure записи в Secure область памяти произойдёт Abort. Abort — это тип исключения ARM, возникающий при невозможности доступа к какому-то устройству или области памяти.

Чаще всего будет происходить Asynchronous Data Abort, или по-русски, асинхронный аборт. Не стоит обсуждать это за ланчем.

Data Abort — потому что он произошёл при чтении/записи данных, а не инструкций процессора. Асинхронный он потому, что происходит не сразу в момент ошибки, а через некоторое время после неё. И вот с этого места будет ещё подробнее. Вообще, при нарушении доступа может произойти как синхронный, так и асинхронный аборт.

Например, когда Linux загружает приложение, он может загрузить его не целиком, разместив при этом только часть страниц в физической памяти, а остальные настроить на генерацию Abort в момент доступа. Приложение запустится, и когда дело дойдёт до незагруженной в физическую память страницы, произойдёт синхронный аборт. Он синхронный потому, что произойдёт ровно на той инструкции, которая совершила обращение к памяти. Когда процессор попадёт в режим Abort, Linux подгрузит нужную страницу памяти и вернёт управление на ту же инструкцию, которая вызвала Abort. Результат — программа продолжит работать "как не бывало".

Но в случае с TrustZone все не так гладко. Некоторые процессоры будут генерировать синхронные исключения, но большинство — будут генерировать для большинства ошибок доступа асинхронный Abort.

В принципе, процессоры ARMv7A, имеющие одновременно Security Extensions и Virtualization Extensions, можно настроить на генерацию синхронных Abort. Это, например, Cortex-A17, но основная масса ARMv7A (по количеству производимых чипов) виртуализации не имеет.

Ответим себе на два вопроса:

  • Почему происходит именно асинхронный аборт?
  • Чем это плохо?

Почему асинхронный?

Начнём с того, что ARMv7A — архитектура с конвейером команд, где инструкции заранее разбиваются процессором и выполняются не строго последовательно. Исполнение части инструкций может происходить параллельно с другими. Например:

	STR r1, [r2]	// *r2 = r1;
	ADD r2, r2, #16 // r2 = r2 + 16;

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

Далее, у процессора есть cache, в котором записанная ячейка застрянет на неопределённое количество времени, и ошибка доступа потенциально произойдёт только в момент синхронизации кеша с памятью.

Потом, даже если область памяти не кешируется: память в ARM делится на Normal, Strongly Ordered и Device Memory, допуская разные вольности со стороны процессора по изменению порядка реальных обращений к памяти и устройствам через AXI. В результате, транзакция через AXI может произойти не сразу из-за того, что доступ к устройству занят другим обращением.

Ну и наконец, если доступ к обычному Bus Slave вызвал Abort, то это будет внешний по отношению к процессорному ядру логический сигнал. Ядро никак не ожидает, что этот сигнал синхронизирован с тем, что происходит сейчас в конвейере команд, и это абсолютно справедливо: ядро даже не может на 100% определить причину такого аборта.

При любом из этих обстоятельств ARM сгенерирует Asynchronous Abort, сообщая нам, что попытка запрещённого доступа была, но, сколько тактов или инструкций назад — он не знает.

Чем плох Asynchronous Abort?

Да тем, что мы не можем определить точку сбоя и не можем ничего исправить. Программа после ошибочного доступа может выполняться не один десяток тактов и за это время удалиться настолько далеко от правильного функционирования, что её останется только остановить и перезапустить. Возможно, и с полным сбросом процессора, если от работы программы после Abort пострадает какая-нибудь периферия или внутренние структуры ОС.

… и какой из этого можно сделать вывод

При работе с TrustZone поначалу возникает соблазн использовать эту технологию как технологию аппаратной виртуализации. Но из-за Asynchronous Abort это не получится сделать.

Действительно, есть два режима: Secure и Non-Secure. Режим Secure может создать для Non-Secure аналог песочницы, ограничить доступ к периферии.

Однако следующим шагом будет виртуализация части периферии, например, Flash-памяти, с которой работают и гостевая ОС, и гипервизор. И тут мы натыкаемся на то, что невозможно просто так взять и закрыть доступ к устройству для гостевой ОС.

Как бы хотелось:

  • гостевая ОС обращается к устройству, происходит Abort (синхронный);
  • гипервизор понимает, что произошло;
  • гипервизор эмулирует ожидаемую гостевой ОС работу устройства;
  • гипервизор возвращает управление гостевой ОС, та продолжает работать, как ни в чем ни бывало.

А вот, как получится:

  • гостевая ОС обращается к устройству, создаются условия для Асинхронного аборта;
  • гостевая ОС продолжает работать, не подозревая об этом;
  • внезапно для всех Abort генерируется системой;
  • гипервизор понимает, что Abort — асинхронный, и он не может вычислить, из-за какой инструкции это произошло, по какому адресу и к какому устройству был доступ;
  • гипервизор прекращает работу гостевой ОС.

Вывод: технологию TrustZone нельзя саму по себе использовать для аппаратной виртуализации.

Можно заставить гостевую ОС стучаться в Secure OS для доступа к запрещённым устройствам, и это основной способ разделения устройств между Secure OS и гостевой ОС. Но об этом мы поговорим в следующий раз.

А память-то, память?

А что обстоит с доступом к обычной памяти? Можно ли выделить для Secure-доступа часть системной DDRAM?

ARM позаботилась об этом меньше, чем можно ожидать!

Контроллеры памяти бывают разные, например,

  • контроллер статической памяти, SRAM, часто это внутренняя память SoC;
  • контроллер динамической памяти, например, DDR3;
  • универсальный контроллер доступа к параллельной памяти, может использоваться для SRAM, NOR Flash.

Все эти контроллеры — типичные Bus Slave. ARM их не разрабатывает, поэтому разграничение доступа Secure/Non-Secure ложится на плечи разработчика SoC, по приведенной выше схеме.

Самый базовый вариант есть почти всегда — доступ к встроенной SRAM настраивается как Secure, а к DDR — как Non-Secure.

Это довольно безопасный способ, потому что все Secure-данные хранятся внутри чипа, не покидают его периметр. Но встроенная SRAM — это жалкие десятки или сотни килобайт, и этого может не хватить для полноценной Secure OS и защищаемых данных.

Более гибкий способ появляется, если производитель SoC по своему усмотрению реализовал контроллер DDR с поддержкой зонирования памяти по критерию NS=0/1. На самом деле, вариантов реализации может быть много, но это не меняет сути.

В целом, такая память предлагает минимум следующее:

  • Есть зоны с разными правами доступа, числом от 3.
  • Одну зону можно настроить как Non-Secure, там будет работать Linux или другая гостевая ОС. Это самая большая часть памяти.
  • Другую зону можно настроить как Secure, там будут данные Secure OS. Эта зона значительно меньше по размеру.
  • Третью зону настраиваем с доступом как Secure, так и Non-Secure. Она используется для обмена большими объёмами данных между Linux и Secure OS, это всего несколько Мб.
  • Более гибкие настройки позволят сделать области Secure Write/Non-Secure Read и, наоборот, для однонаправленного обмена данными.

По счастью, производители действительно включают в свои SoC подобные контроллеры.

Жаль только, что ARM не позаботилась об этом, и мы имеем самые разные решения.

У этой реализации есть минус: поскольку обычная память программ и данных в ARM кешируется, а контроллер памяти — обычный Bus Slave, мы можем далеко не сразу узнать, что произошла запись по запрещённому адресу. Произойдёт асинхронный Abort, и нам останется только убирать обломки программы.

Заключение

В этой статье мы рассмотрели аппаратную реализацию TrustZone в ARMv7A и развеяли некоторые заблуждения, связанные с этой технологией.

Рассмотрены:

  • режимы Secure и Non-Secure;
  • работа одного и нескольких ядер;
  • работа с периферией через AXI;
  • работа с периферией, разработанной без поддержки TrustZone;
  • типы возникающих ошибок доступа;
  • разграничение доступа к физической памяти.

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

Кстати, у нас сейчас открыта вакансия технического писателя. Если Вы ищете работу, и Вам интересна тема этой статьи - присылайте резюме.