Обнаружение уязвимостей Meltdown и Spectre обратило всеобщее внимание на риски, связанные с совместным использованием общего адресного пространства. Даже не смотря на то, что есть специальные механизмы защиты, встроенные в железо, которые должны предотвращать доступ к конфиденциальным данным, очень часто эти уязвимости все равно удается использовать, чтобы эти данные вытащить. Поэтому вполне естественно, что первоначальные стратегии по снижению такого рода рисков включали в себя ограничение совместного использования адресных пространств, но это еще далеко не все, что здесь можно предпринять, и интерес к этой теме не угасает. И вот этот набор патчей, опубликованный Джунаидом Шахидом (Junaid Shahid) (содержащий работу Офира Вайса (Ofir Weisse) и вдохновленный более ранними патчами Александра Шартра (Alexandre Chartre)), как раз содержит то, что необходимо ядру для создания генерализованного механизма изоляции адресного пространства (address-space isolation — ASI).
Защита данных с помощью изоляции адресного пространства
Уязвимости спекулятивного выполнения (speculative execution) возникают, когда можно обхитрить процессор, заставив его в спекулятивном режиме получить доступ к произвольной памяти, минуя проверки, которые (предположительно) существуют в коде для предотвращения такого доступа. Всякий раз, когда становится ясно, что процессор совершил ошибочное предсказание ветвления, последствия спекулятивного выполнения будут отменены, но в различных аппаратных кэшах все-таки останутся его следы. Враждебный код может искать эти следы и использовать их для эксфильтрации данных, которые в противном случае были бы недоступны для злоумышленников.
Однако эти атаки не могут работать с памятью, которая недоступна во время атаки. Вот почему изоляция таблицы страниц ядра (kernel page-table isolation) очень эффективна против Meltdown; если память ядра не отображена во время выполнения кода злоумышленника, то спекулятивное выполнение ее не сольет. Ограничения отображения адресного пространства ядра достаточно для защиты от Meltdown-эксплойтов, работающих в пользовательском пространстве.
Атаки Spectre, напротив, нацелены на систему, когда она работает в режиме ядра (kernel mode), и вся память ядра отображается, поэтому текущая реализация изоляции таблицы страниц ядра никак от нее не защищает. Но на практике ядру почти никогда не требуется доступ ко всему его адресному пространству, и зачастую оно практически не обращается к нему. Отсюда и проистекает интерес к более широкому использованию изоляции адресного пространства: закрывая от ядра его собственную память, когда в этой памяти нет необходимости, изоляция адресного пространства может предотвратить множество возможных атак.
Существуют и другие способы блокирования атак Spectre, некоторые из которых уже реализованы в ядре, но многие из актуальных средств защиты от атака такого рода достаточно затратны и неполны. Очистка кэшей памяти при каждом возвращении в пользовательское пространство блокирует многие эксплойты, но, увы, идет рука об руку со значительными затратами времени выполнения. Администраторам также нужно отключать одновременную многопоточность (simultaneous multi-threading - SMT), что может сильно снизить производительность, или же оставлять открытой возможность атак с родственного процессора. Если изоляцию адресного пространства удастся сделать достаточно эффективной для блокировки атак, возможно, нам удастся отказаться от текущих мер по предотвращению этих атак и вернуть процессору немного производительности.
Где и может в полной мере себя проявить улучшенная изоляция адресного пространства, так это в виртуализации. Виртуальным машинам, работающим под управлением KVM, часто необходимо перехватывать ядро хоста для выполнения различных задач, но эти запросы обычно можно обрабатывать без доступа к большей части адресного пространства ядра. Поскольку на виртуальных машинах вполне может таиться вредоносный код, и они могут работать в системах со смешанными клиентами (mixed-tenant), защита от атак Spectre из этого источника уже давно представляет особый интерес у сообщества. Поэтому неудивительно, что текущие патчи для изоляции адресного пространства приходят от облачных провайдеров (изначально Oracle, а теперь Google) и затрагивают конкретно KVM, хотя сам механизм был спроектирован более генерализованным.
Конфиденциальная и неконфиденциальная память
Основная идея этого набора патчей заключается в концепции “классов изоляции адресного пространства”, каждый из которых описывает определенный контекст безопасности. Неограниченный (unrestricted) класс предназначен для ядра с полным доступом ко всему адресному пространству — другими словами, так, как сейчас и работает ядро. Ограниченные (restricted) классы определяются как подмножество неограниченного класса. Любое отображение таблицы страниц, которое присутствует в ограниченных классах, идентично такому же отображению в неограниченном классе, но в ограниченных классах отсутствуют отображения для большей части конфиденциальных данных, которые отображаются в неограниченном классе.
Ожидается, что система, реализующая изоляцию адресного пространства, будет работать в ограниченном классе практически все время, когда выполняется код пользовательского пространства. Специальный класс изоляции адресного пространства для KVM, будет введен, например, перед передачей управления ядру, работающему в гостевой системе. Класс изоляции таблицы страниц ядра (если он существует — набор патчей описывает такую возможность, но не содержит реализацию) вместо этого будет введен перед возвратом в пространство пользователя системы-хоста. Но важным аспектом изоляции адресного пространства является то, что ограниченные классы также могут использоваться при работе в ядре, если доступ к конфиденциальным данным не требуется. Таким образом, например, ядро может обрабатывать многие задачи, связанные с KVM, вообще не покидая класс изоляции адресного пространства для KVM.
В наборе патчей определено три уровня конфиденциальности для данных, хранящихся в памяти:
“Конфиденциальная” (sensitive) память, которая ни в коем случае не должна утекать из ядра.
“Локально неконфиденциальная” (locally non-sensitive) память, утечка которой при в текущий процесс не смертельна, но не может быть допущена дальнейшая утечка.
“Глобально неконфиденциальная” (globally non-sensitive) память может утекать куда угодно без каких-либо неприятных последствий.
Когда используется изоляция адресного пространства, конфиденциальная память отображается только во время работы ядра и только в том случае, если ядро действительно нуждается в ней. Глобально неконфиденциальная память может оставаться отображенной все время. В отличие от двух других классов, локально неконфиденциальная память различается для каждого отдельного процесса; она может отображаться во время выполнения текущего процесса, но не отображается в адресное пространство какого-либо другого процесса.
Эта классификация памяти применяется ко всем ограниченным классам изоляции адресного пространства; если таких классов много, все они одинаково ограничивают адресное пространство ядра. Другими словами, память, конфиденциальная для одного ограниченного класса, конфиденциальна для всех остальных. Разница между классами изоляции адресного пространства сводится к тому, какая часть пользовательского пространства отображается, и к набору хуков, которые запускаются всякий раз, когда ядро входит или выходит из одного из этих классов. Например, для входа в класс KVM требуется очистить кэши памяти, чтобы предотвратить возможную атаку Spectre, которая может таиться на виртуальной машине. Если бы текущий механизм изоляции таблицы страниц ядра был реализован в этой схеме, этому классу не нужно было бы выполнять очистку кэша при входе, поскольку достаточно просто удалить отображения адресного пространства ядра.
В этот набор патчей встроено интересное решение: если ядро попытается получить доступ к конфиденциальным данным, работая под ограниченным классом изоляции адресного пространства, стригерится ловушка процессора. Одной из возможных реакций на такую ситуацию был бы oops ядра, что, безусловно, предотвратило бы спекулятивные атаки на эти данные. Вместо этого набор патчей для изоляции адресного пространства спровоцирует выход из текущего класса изоляции и в этом случае перейдет в неограниченный режим, что позволит продолжить доступ. Такого ответа должно быть достаточно, чтобы заблокировать спекулятивный доступ к этим данным (поскольку спекулятивное выполнение просто остановится, а не вызовет ловушку), и в то же время не будет мешать легитимному доступу к ядру.
Одна из причин такого подхода должна быть вполне очевидна: адресное пространство ядра огромно, и реальные шансы правильно определить конфиденциальность каждой структуры данных, используемой ядром, и пометить ее соответствующим образом, стремятся к нулю. Кроме того, злоумышленник должен был бы найти все места, где ядро должно выйти из ограниченного класса, чтобы работать с конфиденциальными данными. Даже если бы кто-то достиг всего этого, абсолютно исключена какая-либо надежда сохранять это с течением времени. Таким образом, эта архитектура воплощает в себе четкое понимание того, что невозможно правильно пометить все данные и все места, где должны использоваться конфиденциальные данные, поэтому нужен способ, чтобы все работало должным образом, не смотря на это.
Классификация памяти
Тем не менее, необходимо пытаться правильно пометить, по крайней мере, наиболее важные и наиболее часто используемые данные; большая часть набора из 47 патчей сосредоточена как раз на этой задаче. Для динамически выделяемой памяти существует новый набор флагов GFP (__GFP_GLOBAL_NONSENSITIVE и __GFP_LOCAL_NONSENSITIVE) для маркировки аллокаций, которые не содержат конфиденциального содержимого. Вызовы аллокатора страниц или блоков могут использовать эти флаги, чтобы поместить результирующую память в желаемый класс; память, выделенная с помощью vmalloc()
, также может быть классифицирована таким образом.
Внутри ядро поддерживает два набора таблиц страниц для собственного адресного пространства. Неограниченные таблицы такие же, как и в обычных ядрах; сюда включено “прямое отображение” (direct map), которое делает доступной всю физическую память в адресном пространстве ядра. Ограниченные таблицы страниц изначально практически не содержат отображений. Всякий раз, когда выполняются глобально неконфиденциальные аллокации, отображения для аллоцированных страниц копируются во второй набор таблиц страниц по тем же адресам. При работе в ограниченном режиме активным вместо первого становится второй набор таблиц страниц, предоставляя доступ к неконфиденциальным данным, не содержа в себе никаких конфиденциальных данных.
Обработка локально неконфиденциальных аллокаций немного сложнее. Когда ядро работает в ограниченном режиме, в таблицах страниц фактически присутствует только глобально неконфиденциальная часть прямого отображения. Поскольку эти данные не являются глобально неконфиденциальными, то существует всего одна ограниченная таблица, которая используется для всех процессов. Но локально неконфиденциальные отображения должны быть уникальными для каждого процесса, поэтому они не могут находиться в одной глобальной таблице страниц. И встает вопрос о том, куда в адресном пространстве можно приткнуть эти отображения.
Решение, которое в итоге было выбрано, заключалось в том, чтобы просто продублировать прямое отображение, так что у ядра теперь есть два полных отображения для всей физической памяти. В ограниченном режиме первое отображение, как и прежде, содержит глобально неконфиденциальные данные. А локально неконфиденциальные аллокации фиксируются во втором отображении. Опять же, только страницы, которые были специально аллоцированы как локально неконфиденциальные, отображаются в ограниченной версией этого диапазона, и у каждого процесса есть собственная версия этого отображения. В результате локально неконфиденциальные данные для запущенного процесса доступны даже в ограниченном режиме, но недоступны для любого другого процесса.
Это решает проблему за счет уменьшения объема физической памяти, которым может управлять ядро, вдвое, поскольку каждая физическая страница теперь должна отображаться дважды.
Для статических переменных существует отдельный набор флагов с именами вроде __asi_not_sensitive
и __asi_not_sensitive_readmostly
, которые можно добавлять в объявлении. Как и следовало ожидать, нельзя объявить статические переменные локально неконфиденциальными. Статические (per-CPU) локальные переменные добавляют еще один уровень сложности, что приводит к необходимости использования макросов объявления с такими лаконичными именами, как DEFINE_PER_CPU_SHARED_ALIGNED_ASI_NOT_SENSITIVE()
.
Разработчики не пытались правильно пометить каждую аллокацию и объявление; как отмечалось выше, эта задача не входит в список тех, за которую взялся бы рациональный человек (или даже разработчик ядра). Но все-таки они потратили некоторое время, работая с установленными патчами и отмечая, какие обращения вызывали ловушки и приводили к выходу в неограниченный режим. Выявив и пометив самые загруженные, неконфиденциальные структуры данных, они смогли значительно уменьшить общее влияние этого механизма на производительность.
Изоляция KVM
Имея всю эту инфраструктуру, а также механизм, позволяющий контролировать отображение памяти пользовательского пространства в ограниченный контекст, становится возможным настроить изоляцию адресного пространства, в частности, для KVM, и одновременно отключить некоторые дорогостоящие функции воспрепятствования Spectre атакам. Всякий раз, когда ядро передает управление гостю, оно гарантирует, что режим изоляции адресного пространства KVM включен; при возврате в ядро выход в неограниченный режим может быть и не выполнен, в зависимости от того, что должно делать ядро. Если этого выхода можно избежать (поскольку нет необходимости обращаться к конфиденциальным данным), то тогда мы избежим затрат на очистку кэша. Между тем, с некоторой долей везения, даже враждебная гостевая система не сможет воспользоваться уязвимостями Spectre в ядре хоста.
Однако, чтобы эта защита была полной, ядро должно защитить себя не только от гостя, но и от враждебного процесса, работающего на родственном SMT-узле (сиблинге). В полной реализации, если нужно избежать цены за отключение SMT, это означает “оглушение” (stunning) сиблинга — приостановку его выполнения — пока ядро работает в неограниченном адресном пространстве. Когда ядро возвращается к KVM, родственный процессор должен быть возобновлен ("unstunned"); ядро также должно очищать кэши памяти, чтобы предотвратить утечку данных. Реализация оглушения сиблингов не является частью этого набора патчей; как и в случае оглушения сиблингов (родственников) в реальном мире, существуют возможные последствия, которые необходимо учитывать в первую очередь. В данном случае взаимодействие между оглушением и планировщиком изучено не досконально, поэтому эта работа еще не опубликована.
Конечным результатом всего этого является первый взгляд на форму изоляции адресного пространства, которая может повысить как безопасность, так и производительность для систем, использующих гостевые системы с KVM, и которую можно расширить, чтобы охватить любое количество других ситуаций, в которых может быть полезен этот механизм. На момент написания этой статьи набор патчей еще не получил никаких комментариев, что может быть результатом размера и сложности работы в целом. Она представляет собой интересный набор компромиссов; она может улучшить как производительность, так и безопасность, но за счет большого количества кода и постоянного обслуживания аннотаций конфиденциальности. В мире, где можно надеяться, что в ближайшее время аппаратное обеспечение перестанет содержать Spectre уязвимости, эта цена вполне может показаться слишком высокой. Если же мы полагаем, что Spectre может преследовать нас еще долгое время, то ее целесобразноть вполне очевидна.
Сегодня вечером в OTUS состоится открытое занятие «Знакомство с Nginx. Веб-сервер на Linux». На уроке познакомимся с веб-сервером Nginx, посмотрим основные принципы конфигурации и диагностики. Поговорим о базовых принципах взаимодействия веб-сервера и браузера. Регистрация для всех желающих по ссылке.