Как стать автором
Обновить
0
Virtuozzo
Разработчик ПО для контейнерной виртуализации

CVE и квадратная вероятность

Время на прочтение 10 мин
Количество просмотров 2.1K
Приблизительно год назад, с июля 2019 года нам в OpenVz начали поступать странные багрепорты на RHEL7-based kernel. На первый взгляд баги были разными: ноды крашились в разных местах и даже в разных подсистемах, но каждый раз расследование обнаруживало тот или иной «кривой» объект. Объекты были разными, иногда там обнаруживался какой-то мусор, иногда ссылка в освобожденную память, иногда сам объект оказывался освобожден, но во всех случаях память под этот объект выделялась из kmalloc-192 cache. Под катом – подробный рассказ про эту историю.

image

Обычный траблшутинг в таких случаях – внимательно исследовать жизненный цикл пострадавшего объекта: посмотреть, как под него выделяется память, как она освобождается, насколько правильно берутся и отпускаются reference counters, обращая особое внимание на error-пути. Однако в нашем случае карраптились разные объекты, и проверка их жизненного цикла багов не обнаруживала.

kmalloc-192 cache довольно популярен в ядре, он объединяет несколько десятков различных объектов. Ошибка в жизненном цикле одного из них – наиболее вероятная причина такого рода багов. Даже просто перечислить все такие объекты довольно проблематично, а о том, чтобы все их проверить, даже речи не идет. Багрепорты продолжали поступать, а обнаружить их причину расследованием «в лоб» нам так и не удавалось. Требовалась подсказка.

С нашей стороны эти баги расследовал Андрей Рябинин, специалист по memory management, широко известный в узких кругах kernel developers как разработчик KASAN – офигительной технологии ловли ошибок обращения к памяти.



В сущности, именно KASAN наилучшим образом подходил для обнаружения причин нашего бага.

В оригинальный RHEL7 kernel KASAN не вошел, однако Андрей портировал необходимые патчи к нам в OpenVz. В боевую версию нашего ядра мы KASAN не включили, но в debug-версии ядра он присутствует и активно помогает нашему QA в поиске багов.

В debug-ядро помимо KASAN включено множество других debug фич, унаследованных нами от Red Hat. В результате debug ядро получилось довольно медленным. QA говорит, что те же самые тесты на debug ядре идут в 4 раза дольше. Для нас это непринципиально, мы там не performance измеряем, а баги ищем. Однако для клиентов такой slowdown оказывался неприемлемым, и наши просьбы проставить debug ядро в production неизменно отклонялись.

В качестве альтернативы KASAN клиентов просили включить на пострадавших нодах slub_debug. Эта технология также позволяет выявлять memory corruption. Используя для каждого объекта red zone и memory poisoning, memory allocator при каждом выделении и освобождении памяти проверяет, все ли в порядке. Если что не так, он выдает error message, по возможности исправляет обнаруженные повреждения и позволяет ядру продолжать работу. Кроме того, сохраняется информация о том, кто в последний раз аллоцировал и освобождал объект, так что в случае post-factum обнаружения memory corruption можно понять, «кем» этот объект был в «прошлой жизни». Slub_debug можно включить в kernel commandline на боевом ядре, но эти проверки также потребляют память и ресурсы cpu. Для отладки в development и QA это вполне приемлемо, но клиенты в production используют это без особого энтузиазма.

Прошло полгода, приближался Новый Год. Локальные тесты на debug ядре с KASAN проблему так и не поймали, багрепортов от нод с включенным slub_debug нам так и не поступило, в сырцах мы ничего найти не смогли и проблему так и не раскопали. Андрея загрузили другими задачами, у меня, наоборот, образовался просвет и очередной багрепорт поручили разбирать мне.

Разобрав crash dump, я вскоре обнаружил проблемный kmalloc-192 объект: его память была заполнена каким-то мусором, информацией, принадлежащим другому типу объектов. Это было сильно похоже на последствия use-after-free, однако внимательно просмотрев в сырцах жизненный цикл поврежденного объекта, я тоже не нашел ничего подозрительного.

Проглядел старые багрепорты, попробовал найти какую-нибудь зацепку там, но тоже безрезультатно.

В конце концов я вернулся к своему багу и стал разглядывать предыдущий объект. Он тоже оказался in-use, однако по его содержимому было совершенно непонятно что это такое – там не было констант, ссылок на функции или другие объекты. Отследив несколько поколений ссылок на этот объект, я в конце концов выяснил, что это был shrinker bitmap. Этот объект был частью технологии оптимизации освобождения контейнерной памяти. Технология изначально разрабатывалась для наших ядер, позднее её автор Кирилл Тхай закоммитил ее в linux mainline.

«The results show the performance increases at least in 548 times.»

Несколько тысяч подобных патчей дополняют оригинальный rock-stable RHEL7 kernel, делая Virtuozzo kernel максимально удобным для хостеров. По возможности мы стараемся заслать наши наработки в mainline, поскольку это облегчает поддержание кода в исправном состоянии.

Пройдя по ссылкам, я нашел структуру, описывающую мой bitmap. Descriptor полагал, что размер bitmap должен быть 240 байт, и это никак не могло быть правдой, поскольку фактически объект выделялся из kmalloc-192 cache.

Bingo!

Получалось что работающие с bitmap функции обращались к памяти за его верхней границей и могли менять содержимое следующего объекта. В моем случае в начале объекта был refcount, и когда bitmap его занулял, последующий put приводил к внезапному освобождению объекта. Позднее память выделялась заново под новый объект, инициализация которого воспринималась как мусор кодом старого объекта, что рано или поздно неизбежно приводило к крашу ноды.

image

Хорошо, когда можно проконсультироваться с автором кода!

Разглядывая с Кириллом его код, мы вскоре нашли первопричину обнаруженной нестыковки. При увеличении числа контейнеров bitmap должен был увеличиваться, однако один из случаев мы упустили из рассмотрения и в результате иногда пропускали resize bitmap. В наших локальных тестах такая ситуация не обнаружилась, а в версии патча, которую Кирилл заслал в mainline, код был переработан, и бага там не было.

С 4 попытки совместными усилиями мы с Кириллом сочинили вот такой патч, с месяц гоняли его в локальных тестах и в конце февраля выпустили апдейт c исправленным ядром. Выборочно проверили другие крашдампы, тоже обнаружили по соседству неправильный bitmap, отпраздновали победу и под шумок списали старые баги.

Однако старушки все падали и падали. Ручеёк таких рода багрепортов сократился, но полностью не иссяк.

В общем-то, это было ожидаемы. Наши клиенты – хостеры. Они сильно не любят ребутить свои ноды, потому что reboot == downtime == потерянные деньги. Мы тоже не любим часто релизить ядра. Официальный выпуск апдейта довольно трудозатратная процедура, требующая прогона кучи различных тестов. Поэтому новые стабильные ядра выпускаются приблизительно раз в квартал.

Чтобы обеспечить оперативную доставку багфиксов на клиентские prоduction-ноды мы используем ReadyKernel live patches. По-моему, кроме нас так больше никто не делает. В Virtuozzo 7 используется необычная стратегия применения live patсhes.

Обычно лайфпатчат только security. У нас же 3/4 исправлений – это багфиксы. Исправления багов, на которые наши customer'ы уже наткнулись или могут запросто наткнуться в будущем. Эффективно такие вещи можно делать только для своего дистрибутива: без feedback'а от пользователей не понять, что им важно, а что нет.

Live patching, конечно, не панацея. Все подряд вообще невозможно лайфпатчить – технология не позволяет. Новый функционал таким образом тоже не добавляют. Однако заметная часть багов фиксится простейшими однострочными патчами, которые превосходно подходят для лайфпатчинга. В более сложных случаях оригинальный патч приходится «творчески доработать напильником», иногда глючит live-patching машинерия, однако наш кудесник лайфпатчей Женя Шатохин превосходно знает свое дело. Недавно, например, он раскопал феерический баг в kpatch, про который по хорошему вообще стоит отдельному оперу писать.

По мере накопления подходящих багфиксов, обычно раз в одну-две недели Женя запускает в производство очередную серию ReadyKernel live patches. После релиза они моментально разлетаются на клиентские ноды и предотвращают наступание на уже известные нам грабли. И все это без ребута клиентских нод. И без необходимости часто релизить ядра. Сплошные преимущества.

Однако зачастую live patch доезжает до клиентов слишком поздно: закрываемая им проблема уже случилась, но нода, тем не менее, еще не упала.

Именно поэтому появление новых багрепортов с уже исправленной нами проблемой не было для нас чем-то неожиданным. При их разборе раз за разом обнаруживались знакомые симптомы: старое ядро, мусор в kmalloc-192, «неправильный» bitmap перед ним и незагруженный или поздно загруженный live patch с фиксом.

Одним из таких случаев выглядел и OVZ-7188 от FastVPS, поступивший к нам в самом конце февраля. «Большое спасибо за багрепорт. Соболезнуем. Сходу сильно похоже на known issue. Жаль, что в OpenVZ лайв-патчей нет. Ждите релиза стабильного ядра, switch to Virtuozzo или используйте нестабильные ядра с багфиксом.»

Багрепорты – одна из наиболее ценных вещей, которые дает нам OpenVZ. Их исследование дает нам шанс обнаружить серьезные проблемы до того, как на них наступит какой-нибудь из «толстых» клиентов. Поэтому несмотря на known issue, я тем не менее попросил залить нам крашдампы.

Разбор первого из них меня несколько обескуражил: «неправильного» bitmap перед «кривым» kmalloc-192 объектом не обнаружилось.

А чуть погодя проблема воспроизвелась и на новом ядре. А потом еще, еще и еще.

Oops!


Как так? Недофиксили? Перепроверил сырцы — все нормально, патч на месте, ничего не потерялось.

Опять corruption? В том же месте?

Пришлось разбираться по новой.

image
(Что это? смотри сюда)

В каждом из новых крашдампов расследование опять утыкалось в kmalloc-192 объект. В общем и целом, такой объект выглядел вполне нормально, однако в самом начале объекта каждый раз обнаруживался неправильный адрес. Отслеживая взаимосвязи объекта, я обнаружил что в адресе занулялись два внутренних байта.

in all cases corrupted pointer contains nulls in 2 middle bytes: (mask 0xffffffff0000ffff)
0xffff9e2400003d80
0xffff969b00005b40
0xffff919100007000
0xffff90f30000ccc0

В первом из перечисленных случаев вместо «неправильного» адреса 0xffff9e2400003d80 должен был быть «правильный» адрес 0xffff9e24740a3d80. Аналогичная ситуация обнаружилась и в других случаях.

Получалось, что какой-то посторонний код занулял нашему объекту 2 байта. Наиболее вероятный сценарий – use-after-free, когда некий объект после своего освобождения обнуляет какое-то поле в первых своих байтах. Проверил наиболее часто используемые объекты, но ничего подозрительного не нашлось. Опять тупик.

FastVPS по нашей просьбе неделю погонял debug ядро с KASAN, но это не помогло, проблема так и не воспроизвелась. Мы попросили прописать slub_debug, однако это требовало перезагрузки, и процесс сильно затянулся. В марте-апреле ноды падали еще несколько раз, но slub_debug был выключен, и новой информации это нам не дало.

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

Ожидание закончилось 7-го июня – наконец то проблема выстрелила на ядре с включенным slub_debug. Проверяя red zone при освобождении объекта slub_debug обнаружил два нулевых байта за его верхней границей. Другими словами, выяснилось, что это не use-after-free, виновником опять оказался предыдущий объект. Там располагался нормально выглядящий struct nf_ct_ext. Эта структура относится к connection tracking, описанию сетевого соединения, которое использует firewall.

Однако все равно было непонятно, почему так происходило.

Стал вглядываться в conntrack: в один из контейнеров кто-то по ipv6 постучался на открытый 1720 порт. По порту и протоколу я нашел соответствующий nf_conntrack_helper.

static struct nf_conntrack_helper nf_conntrack_helper_q931[] __read_mostly = {
        {
                .name                   = "Q.931",
                .me                     = THIS_MODULE,
                .data_len               = sizeof(struct nf_ct_h323_master),
                .tuple.src.l3num        = AF_INET, <<<<<<<< IPv4
                .tuple.src.u.tcp.port   = cpu_to_be16(Q931_PORT),
                .tuple.dst.protonum     = IPPROTO_TCP,
                .help                   = q931_help,
                .expect_policy          = &q931_exp_policy,
        },
        {
                .name                   = "Q.931",
                .me                     = THIS_MODULE,
                .tuple.src.l3num        = AF_INET6, <<<<<<<< IPv6
                .tuple.src.u.tcp.port   = cpu_to_be16(Q931_PORT),
                .tuple.dst.protonum     = IPPROTO_TCP,
                .help                   = q931_help,
                .expect_policy          = &q931_exp_policy,
        },
};

Сравнивая структуры, я заметил, что ipv6 helper не определял .data_len. Полез в git разбираться, откуда оно такое взялось, обнаружил патч 2012 года.

commit 1afc56794e03229fa53cfa3c5012704d226e1dec
Author: Pablo Neira Ayuso <pablo@netfilter.org>
Date: Thu Jun 7 12:11:50 2012 +0200

netfilter: nf_ct_helper: implement variable length helper private data

This patch uses the new variable length conntrack extensions.

Instead of using union nf_conntrack_help that contain all the
helper private data information, we allocate variable length
area to store the private helper data.

This patch includes the modification of all existing helpers.
It also includes a couple of include header to avoid compilation
warnings.

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

В результате чего получилось, что сonnect по ipv6 на открытый порт 1720 запускал функцию q931_help(), она писала в структуру, под которую никто не выделил память. Простой скан портов занулял пару байт, передача нормального протокольного сообщения заполняла структуру более осмысленной информацией, однако в любом случае перетиралась чужая память и рано или поздно это приводило к крашу ноды.

В 2017 году Florian Westphal очередной раз переработал код и убрал .data_len, а обнаруженная мной проблема осталась незамеченной.

Несмотря на то, что в нынешнем linux kernel mainline обнаруженного бага уже нет, проблема была унаследована ядрами кучи дистрибутивов linux, включая актуальные до сих пор RHEL7/CentOS7, SLES 11 & 12, Oracle Unbreakable Enterprise Kernel 3 & 4, Debian 8 & 9 и Ubuntu 14.04 & 16.04 LTS.

Баг тривиально воспроизвелся на тестовой ноде, и на нашем ядре, и на оригинальном RHEL7. Явный security: удаленно управляемый memory corruption. Там, где открыт 1720 ipv6 порт – практически ping of death.

9-го июня я сделал однострочный патч c невнятным описанием и заслал его в mainline. Подробное описание я отправил в Red Hat Bugzilla и отдельно отписал в Red Hat Security.

Дальше события развивались без моего участия.
15-го июня Женя Шатохин зарелизил ReadyKernel live patch для наших старых ядер.
https://readykernel.com/patch/Virtuozzo-7/readykernel-patch-131.10-108.0-1.vl7/

18-го июня мы выпустили новое стабильное ядро в Virtuozzo и OpenVz.
https://virtuozzosupport.force.com/s/article/VZA-2020-043

24-го июня Red Hat Security присвоил багу CVE id
https://access.redhat.com/security/cve/CVE-2020-14305

Проблема получила moderate impact с необычно высоким CVSS v3 Score 8.1 и в течение следующих нескольких дней на публичный шляпный баг отреагировали и другие дистрибутивы
SUSE https://bugzilla.suse.com/show_bug.cgi?id=CVE-2020-14305
Debian https://security-tracker.debian.org/tracker/CVE-2020-14305
Ubuntu https://people.canonical.com/~ubuntu-security/cve/2020/CVE-2020-14305.html

6-го июля KernelCare выпустил livepatch для affected distributives.
https://blog.kernelcare.com/new-kernel-vulnerability-found-by-virtuozzo-live-patched-by-kernelcare

9-го июля проблему пофиксили в stable Linux kernels 4.9.230 and 4.4.230.
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?h=linux-4.9.y&id=396ba2fc4f27ef6c44bbc0098bfddf4da76dc4c9

Дистрибутивы, однако, дырку так до сих пор и не закрыли.

– Смотри, Костя, – говорю я своему напарнику Косте Хоренко, – у нас снаряд два раза в одну воронку попал! Я и один-то access-beyond-end-of-object последний раз встречал непойми когда, а тут оно нас дважды подряд посетило. Скажи мне, это ж вроде квадратная вероятность? Или не квадратная?
– Вероятность квадратная, да. Но тут надо смотреть – какого события вероятность? Квадратная вероятность того события, что именно 2 раза подряд попались необычные баги. Именно подряд.

Ну, Костя умный, ему виднее.
Теги:
Хабы:
+8
Комментарии 1
Комментарии Комментарии 1

Публикации

Информация

Сайт
www.virtuozzo.com
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия

Истории