Retbleed — это название класса уязвимостей спекулятивного исполнения, связанных с инструкциями возврата. Средства защиты от Retbleed попали в основное ядро, но на момент написания этой статьи некоторые остающиеся проблемы не позволили им попасть в стабильные выпуски обновлений. Защита от Retbleed может сильно снизить производительность, особенно на некоторых процессорах Intel. Томас Глейкснер (Thomas Gleixner) и Питер Зайлстра (Peter Zijlstra) считают, что они нашли лучший способ, который обходит существующие средства защиты и вводит в заблуждение механизмы спекулятивного выполнения процессора.
Для того чтобы процессор мог предсказать значение после инструкции возврата, он должен иметь некоторое представление о том, куда вернется код. В последних моделях процессоров Intel существует специальная скрытая структура данных, называемая «буфером стека возврата» (RSB), которая кэширует адреса возврата для спекуляций. RSB может содержать 16 записей, поэтому он должен отбрасывать самые старые записи, если цепочка вызовов идет глубже этого значения. При возврате глубокой цепочки вызовов RSB может переполниться. Можно было бы подумать, что спекуляция просто прекратится в этот момент, но вместо этого процессор прибегает к другим эвристикам, включая предсказание из буфера истории ветвлений. Увы, техники неправильного обучения буфера истории ветвей на данный момент хорошо изучены.
В результате длинные цепочки вызовов в ядре подвержены атакам спекулятивного выполнения. На процессорах Intel, начиная с поколения Skylake, единственным способом предотвратить такие атаки является включение «функции» процессора indirect branch restricted speculation (IBRS), которая была добавлена Intel в начале эры Spectre. IBRS работает, но у нее есть нежелательный побочный эффект - снижение производительности на целых 30%. По какой-то причине пользователи не испытывают энтузиазма по поводу этого решения.
Другой способ
Глейкснер и Зайлстра решили попробовать другой подход. Спекулятивным выполнением обратных вызовов на этих процессорах можно злоупотреблять только в том случае, если RSB недополнен. Таким образом, если недополнение RSB можно предотвратить, эта конкретная проблема исчезнет. А этого, похоже, можно добиться, «набивая» RSB всякий раз, когда есть риск, что в нем закончатся записи.
Это сразу же приводит к двум новым проблемам: узнать, когда RSB заканчивается, и найти способ наполнить его снова. Первая задача решается путем отслеживания текущей глубины цепочки вызовов приблизительным способом. Система сборки модифицирована для создания пары новых секций в исполняемом образе ядра, чтобы хранить thunk входа и выхода для функций ядра и отслеживать их. Если включена начинка RSB, то при входе в каждую функцию будет вызываться входной thunk, а выходной thunk будет выполняться на выходе.
Состояние RSB отслеживается с помощью 64-битного значения для каждого процессора, которое первоначально установлено в:
0x8000 0000 0000 0000
Функция входа увеличивает этот счетчик, сдвигая его вправо на пять бит. Процессор увеличивает значение по знаку, поэтому после первого вызова счетчик будет выглядеть следующим образом:
0xfc00 0000 0000 0000 0000
Если последовательно произойдет еще двенадцать вызовов, знаковый бит будет сдвинут до упора вправо, и счетчик будет содержать одни единицы, причем биты начнут выпадать с правого конца; таким образом, этот счетчик не сможет надежно считать больше двенадцати. Таким образом, он имитирует RSB, который не может содержать более 16 записей, с запасом прочности в четыре обращения; использование сдвигов позволяет добиться такого поведения без необходимости введения ветвления. Каждый раз, когда выполняется функция возврата, происходит обратное: счетчик сдвигается влево на пять бит. После двенадцати возвратов следующий сдвиг очистит оставшиеся биты, и счетчик будет иметь нулевое значение, что является признаком того, что необходимо что-то сделать, чтобы предотвратить переполнение RSB.
Этим «что-то» является быстрая серия вызовов функций (закодированных на ассемблере и приведенных в конце этого патча), которая добавляет 16 записей в стек вызовов, а значит, и в RSB. Каждый из этих вызовов, при возврате из него, немедленно выполнит инструкцию int3; это остановит спекуляцию, если эти возвратные вызовы когда-либо выполнялись спекулятивно. Конечно, реальное ядро не хочет выполнять эти инструкции (или все эти возвраты), поэтому код RSB-stuffing увеличивает реальный указатель стека на только что добавленные кадры вызовов.
В итоге RSB больше не соответствует реальному стеку вызовов, но в нем полно записей, которые не причинят вреда, если в них спекулировать. В этот момент счетчик глубины вызовов может быть установлен в -1 (все единицы в представлении двойного дополнения), чтобы отразить тот факт, что RSB заполнен. Теперь ядро защищено от эксплуатации Retbleed – до тех пор, пока не произойдет еще одна цепочка из двенадцати возвратов, и в этом случае RSB нужно будет заполнить снова.
Затраты
Довольно много работы было проделано для минимизации накладных расходов этого решения, особенно в системах, где оно не нужно. Ядро собирается с прямыми вызовами своих функций, как обычно; во время загрузки, если выбрана опция retbleed=stuff, все эти вызовы будут исправлены, чтобы проходить через учетные транзакторы. Сами блоки размещены в огромном страничном отображении, чтобы минимизировать накладные расходы на буфер lookaside трансляции. Несмотря на это, как отмечается в сопроводительном письме, существуют издержки: «Мы оба, что неудивительно, весьма ненавидим результат».
Эти издержки проявляются в нескольких формах. Требуется «впечатляющий» объем памяти для хранения банков и связанной с ними домашней работы. Раздувание ядра влияет на производительность, даже в системах, где не включена функция RSB stuffing. Дополнительные инструкции увеличивают нагрузку на кэш инструкций, замедляя выполнение. Последняя проблема может быть несколько смягчена, говорится в сопроводительном письме, если выделять thunks в начале каждой функции, а не в отдельном разделе. Глейкснер подготовил патч для GCC, чтобы сделать это возможным, и сообщает, что при его использовании некоторые потери производительности уменьшаются.
Сопроводительное письмо содержит длинный список эталонных результатов, сравнивающих производительность RSB stuffing с производительностью полного отключения защиты и использования IBRS. Цифры для RSB stuffing поражают воображение, включая 382% регресс производительности для одного микробенчмарка. Однако во всех случаях RSB stuffing работает лучше, чем IBRS.
Однако лучшая производительность по сравнению с IBRS интересна только в том случае, если достигнута главная цель – блокирование атак Retbleed. В сопроводительном письме говорится следующее:
Предполагается, что набивка на 12-м возврате достаточна, чтобы прервать спекуляцию до того, как она попадет в недополнение и откат к другим предикторам. Тестирование подтвердило, что это работает. Йоханнес [Викнер], один из исследователей retbleed, пытался атаковать этот подход и подтвердил, что он снижает соотношение сигнал/шум до уровня хрустального шара.
Очевидно, что нет никаких научных доказательств того, что это выдержит будущие исследования, но все, что мы можем сделать прямо сейчас, это строить догадки на этот счет.
Итак, похоже, что RSB stuffing работает – по крайней мере, пока. Это должно сделать его привлекательным в ситуациях, когда защита от атак Retbleed считается необходимой; хостинг-провайдеры с недоверенными пользователями – один из очевидных примеров. Но никто не будет доволен накладными расходами, даже если они лучше, чем у IBRS. Для многих пользователей RSB stuffing будет рассматриваться как умный хак, который, к счастью, им не придется использовать.