Книга: BPF для мониторинга Linux
Назад: 1. Введение
Дальше: 3. Карты BPF

2. Запуск программ BPF

Виртуальная машина BPF способна выполнять инструкции в ответ на события, выдаваемые ядром. Однако не все программы BPF имеют доступ ко всем событиям, запускаемым ядром. Загружая программу в виртуальную машину BPF, вы должны решить, какой тип программы запускаете. Это информирует ядро о том, где программа будет запущена. При этом верификатор BPF будет знать, какие помощники разрешены в вашей программе. Выбирая тип программы, вы также выбираете интерфейс, который она реализует. Этот интерфейс гарантирует, что у вас есть доступ к соответствующему типу данных, и показывает, может ли программа получать доступ к сетевым пакетам напрямую.

В этой главе мы продемонстрируем, как написать ваши первые программы BPF. А также познакомим вас с различными типами программ BPF (существующими на момент написания этой книги), которые вы можете создать. На протяжении многих лет разработчики ядра добавляли различные точки входа, к которым вы можете присоединять программы BPF. Эта работа еще не завершена, и каждый день появляются новые способы использования BPF. В данной главе мы хотим сосредоточиться на некоторых наиболее полезных типах программ, чтобы дать вам представление о том, чего вы можете достичь с помощью BPF. В следующих главах также будет приведено множество примеров написания программ BPF.

Кроме того, мы рассмотрим роль, которую верификатор BPF играет в запуске ваших программ. Этот компонент проверяет, что ваш код безопасен для выполнения, и помогает вам писать программы, которые не приведут к неожиданным результатам, таким как исчерпание памяти или внезапные сбои ядра. Но начнем мы с основ написания ваших собственных BPF-программ с нуля.

Написание программ BPF

Самый распространенный способ написания BPF-программ — использование подмножества C, скомпилированного с помощью LLVM. LLVM — это компилятор общего назначения, который может генерировать различные типы байт-кода. В нашем случае LLVM выведет код ассемблера BPF, который мы позже загрузим в ядро. Мы не собираемся создавать большую сборку BPF. После долгого обсуждения мы решили, что лучше привести примеры того, как применять его при определенных обстоятельствах, хотя вы можете легко найти несколько ссылок на другие примеры в Интернете или на справочных страницах BPF. В следующих главах мы покажем короткие примеры сборки BPF.

Ядро предоставляет системный вызов bpf для загрузки программ в виртуальную машину BPF после их компиляции. Этот вызов используется не только для загрузки программ, но и для других операций (больше примеров приведено в последующих главах). Ядро также предоставляет несколько утилит, которые избавляют вас от загрузки BPF-программ. В первом примере кода мы используем эти помощники, чтобы показать пример Hello, BPF World:

#include <linux/bpf.h>

#define SEC(NAME) __attribute__((section(NAME), used))

 

SEC("tracepoint/syscalls/sys_enter_execve")

  int bpf_prog(void *ctx) {

  char msg[] = "Hello, BPF World!";

  bpf_trace_printk(msg, sizeof(msg));

  return 0;

}

 

char _license[] SEC("license") = "GPL";

Эта первая программа содержит несколько интересных элементов. Мы используем атрибут SEC, чтобы сообщить виртуальной машине BPF, когда хотим запустить программу. В данном случае мы запустим программу BPF, когда будет обнаружена точка трассировки в системном вызове execve. Точки трассировки — это статические метки в двоичном коде ядра, которые позволяют разработчикам вводить код для проверки работы ядра. Мы подробно поговорим о них в главе 4, а сейчас вам нужно знать только то, что execve — это инструкция, выполняющая другие программы. Итак, мы увидим сообщение Hello,BPFWorld! каждый раз, когда ядро обнаружит, что программа выполняет другую программу.

В конце примера мы указываем лицензию для этой программы. Поскольку ядро Linux лицензируется как GPL, оно может загружать только программы с такой же лицензией. Если установить какую-либо другую лицензию, ядро откажется загружать нашу программу. Мы применим bpf_trace_printk для печати сообщений в журнале трассировки ядра, который находится в папке /sys/kernel/debug/tracing/trace_pipe.

Используем clang для компиляции нашей первой программы в правильный двоичный файл ELF. Именно этот формат ядро ожидает для загрузки. Сохраним нашу первую программу в файле с именем bpf_program.c, чтобы затем скомпилировать:

clang -O2 -target bpf -c bpf_program.c -o bpf_program.o

Вы найдете несколько сценариев для компиляции этих программ в репозитории GitHub (oreil.ly/lbpf-repo), поэтому не нужно дословно запоминать синтаксис команды clang.

Теперь, когда мы скомпилировали нашу первую программу BPF, нужно загрузить ее в ядро. Как уже упоминалось, для этого используем специальный помощник, который предоставляется ядром, чтобы освободить стандартный шаблон компиляции и загрузки программы. Этот помощник называется load_bpf_file, он берет двоичный файл и пытается загрузить его в ядро. Вы можете найти помощник в репозитории GitHub, как и все примеры из книги. Все это показано в файле bpf_load.h:

#include <stdio.h>

#include <uapi/linux/bpf.h>

#include "bpf_load.h"

 

int main(int argc, char **argv) {

  if (load_bpf_file("hello_world_kern.o") != 0) {

    printf("The kernel didn't load the BPF program\n");

    return -1;

  }

 

  read_trace_pipe();

 

  return 0;

}

Мы собираемся использовать сценарий, чтобы скомпилировать программу и скомпоновать ее как двоичный файл ELF. В данном случае не нужно указывать цель, потому что эта программа не будет загружена в виртуальную машину BPF. Следует подключить внешнюю библиотеку, и написание сценария облегчит задачу объединения:

TOOLS=../../../tools

INCLUDE=../../../libbpf/include

HEADERS=../../../libbpf/src

clang -o loader -l elf \

  -I${INCLUDE} \

  -I${HEADERS} \

  -I${TOOLS} \

  ${TOOLS}/bpf_load.c \

  loader.c

Если вы захотите запустить эту программу, то можете выполнить двоичный файл с помощью sudo./loader. sudo — это команда Linux, которая даст вам права root для выполнения определенной команды на вашем компьютере. Если вы не воспользуетесь sudo, то получите сообщение об ошибке, поскольку большинство BPF-программ могут быть загружены в ядро только пользователем с правами root.

Запустив эту программу, через несколько секунд вы увидите сообщение Hello,BPFWorld!, даже если ничего не делаете на компьютере. Это связано с тем, что программы, работающие в фоновом режиме, могут сейчас запускать другие программы.

Когда вы останавливаете данную программу, сообщение перестает отображаться в вашем терминале. BPF-программы выгружаются из ВМ, как только завершаются программы, которые их загрузили. В следующих главах мы рассмотрим, как сделать работу BPF-программы постоянной даже после завершения работы загрузчика, но пока не будем слишком углубляться. Это важная концепция, которую нужно учитывать, потому что во многих ситуациях важно, чтобы программы BPF запускались в фоновом режиме и собирали данные из системы независимо от того, запущены ли другие процессы.

Теперь, когда вы узнали о базовой структуре программ BPF, рассмотрим, какие программы вы можете написать, чтобы получить доступ к различным подсистемам ядра Linux.

Типы программ BPF

Хотя четкого деления программ на категории не существует, вы скоро поймете, что все их типы, описанные в этом разделе, распределены на две категории в зависимости от основного назначения.

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

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

Список типов программ, о которых мы поговорим далее, не разделен на категории — представим их в хронологическом порядке, в котором они были добавлены в ядро. Мы переместили наиболее редко используемые программы в конец раздела и пока сосредоточимся на наиболее полезных. Если вас интересует какая-либо программа, о которой здесь подробно не говорится, можете узнать о ней больше, введя команду man2bpf (oreil.ly/qXl0F).

Программы сокетной фильтрации

Программы типа BPF_PROG_TYPE_SOCKET_FILTER были добавлены в ядро Linux в числе первых. Привязывая программу BPF к открытому сокету, вы получа­ете доступ ко всем пакетам, проходящим через него. Программы с сокетными фильтрами не позволяют вам изменять содержимое этих пакетов или их назначение — они дают доступ к ним только для наблюдения. Метаданные, которые получает ваша программа, содержат относящуюся к сетевому стеку информацию, такую как тип протокола, используемого для доставки пакета.

Мы рассмотрим сокетную фильтрацию и другие сетевые программы более подробно в главе 6.

Программы kprobe

Как вы увидите в главе 4, в которой рассматривается трассировка, kprobes — это функции, которые можно динамически подключать к определенным точкам вызова в ядре. Программы типа BPF kprobe позволяют использовать программы BPF в качестве обработчиков kprobe. Они определены как тип BPF_PROG_TYPE_KPROBE. Виртуальная машина BPF гарантирует, что программы kprobe всегда безопасны при запуске, что является преимуществом по сравнению с традиционными модулями kprobe. Однако нужно помнить: kprobes не считаются стабильными точками входа в ядро, поэтому необходимо убедиться, что ваши программы kprobe совместимы с версиями ядра, которые вы используете.

Создавая программу BPF, которая связана с kprobe, вы должны решить, как она станет выполняться — в качестве первой инструкции в вызове функции или когда вызов завершится. Следует указать это в разделе заголовка программы BPF. Например, если вы хотите проверить аргументы, когда ядро создает системный вызов exec, то подключаете программу в начале вызова. В этом случае нужно прописать заголовок раздела SEC("kprobe/sys_exec"). Если же хотите проверить возвращаемое значение системного вызова exec, требуется написать в заголовке раздела SEC("kretprobe/sys_exec").

В последующих главах книги мы гораздо подробнее обсудим kprobes. Они станут фундаментом для понимания трассировки с помощью BPF.

Программы трассировки

Программы этого типа позволяют вам подсоединить BPF-программы к обработчику трассировки, предоставляемому ядром. Программы трассировки определяются как тип BPF_PROG_TYPE_TRACEPOINT. Как вы увидите в главе 4, точки трассировки — это статические метки в кодовой базе ядра, которые позволяют вводить произвольный код для выполнения трассировки и отладки. Они менее гибки, чем kprobes, потому что должны быть определены ядром заранее, но гарантированно стабильны после введения в ядро соответствующей точки отладки или модуля. Это обеспечивает гораздо более высокий уровень предсказуемости при отладке системы.

Все точки трассировки в вашей системе определены в каталоге /sys/kernel/debug/tracing/events. Там вы найдете все подсистемы, включающие в себя любые точки трассировки, к которым вы можете подключить программу BPF. Еще один интересный факт состоит в том, что BPF объявляет собственные точки трассировки, поэтому вы можете писать программы BPF, которые проверяют поведение других программ BPF. Точки трассировки BPF определены в /sys/kernel/debug/tracing/events/bpf. Там можно найти, например, определение точки трассировки для bpf_prog_load. Это означает, что вы можете написать программу BPF, которая срабатывает при загрузке других программ BPF.

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

Программы XDP

Программы XDP позволяют вам писать код, который выполняется на самом первом этапе, как только сетевой пакет поступает в ядро. Они определены как тип BPF_PROG_TYPE_XDP. Здесь предоставляется ограниченный набор информации из пакета, учитывая, что у ядра было немного времени для ее обработки. Поскольку пакет исследуется и выполняется на ранней стадии, вы можете намного лучше контролировать его обработку.

Программы XDP реализуют несколько действий, которыми вы можете управлять и которые позволяют решать, что делать с пакетом. Можете вернуть XDP_PASS из своей программы XDP, что означает: пакет должен быть передан следующей подсистеме в ядре. Вы также можете вернуть XDP_DROP, что означает: ядро должно полностью игнорировать этот пакет и больше ничего с ним не делать. А еще можете вернуть XDP_TX, что означает: пакет должен быть направлен обратно в сетевую интерфейсную карту (NIC), через которую был получен.

Такая степень контроля дает возможность использовать много интересных программ на сетевом уровне. XDP стал одним из основных компонентов BPF, поэтому мы включили в книгу главу о нем. В главе 7 мы рассмотрим сценарии использования XDP, такие как реализация программ для защиты вашей сети от атак распределенного отказа в обслуживании (DDoS).

Программы Perf Event

Такие программы BPF позволяют вашим программам понять и воспринять события Perf. Они определены как тип BPF_PROG_TYPE_PERF_EVENT. Perf — это внутренний профилировщик ядра, который генерирует события, предоставляющие данные о производительности для аппаратного и программного обеспечения. Вы можете использовать его для мониторинга многих компонентов: от процессора вашего компьютера до любого программного обеспечения, работающего в вашей системе. После связывания программы BPF с событиями Perf ваш код будет выполняться каждый раз, когда Perf генерирует данные для анализа.

Программы для сокетов контрольных групп

Эти программы позволяют вам связать логику BPF с контрольными группами (cgroups). Они имеют тип BPF_PROG_TYPE_CGROUP_SKB. Это дает возможность механизму контрольных групп (cgroups) контролировать сетевой трафик процессов, которые к ним относятся. С помощью программ для сокетов контрольных групп вы можете решить, что делать с сетевым пакетом до его доставки процессу в контрольной группе. Любой пакет, который ядро пытается доставить любому процессу в той же группе, пройдет через один из этих фильтров. В то же время вы можете решить, что предпринять, когда процесс в контрольной группе отправляет сетевой пакет через данный интерфейс.

Как видите, их поведение аналогично действиям программ BPF_PROG_TYPE_SOCKET_FILTER. Основное различие состоит в том, что программы BPF_PROG_TYPE_CGROUP_SKB привязаны ко всем процессам внутри группы, а не к конкретным процессам. Такое поведение применяется ко всем существующим и будущим сокетам, созданным в данной группе. BPF-программы, связанные с контрольными группами, особенно полезны в контейнерных средах, где группы процессов ограничены контрольными группами и где вы можете применять одинаковые политики к ним всем, не идентифицируя каждую по отдельности. Cillium (github.com/cilium/cilium) — популярный проект с открытым исходным кодом, который предоставляет возможности Kubernetes для балансировки нагрузки и обеспечения безопасности, — широко использует программы сокетов контрольных групп для применения своих политик в группах, а не в изолированных контейнерах.

Программы Cgroup Open Socket

Программы этого типа позволяют вам выполнять код, когда какой-либо процесс в контрольной группе открывает сетевой сокет. Такое поведение подобно поведению программ, подключенных к буферам сокетов контрольных групп, но вместо того, чтобы предоставлять доступ к пакетам, когда они поступают через сеть, они позволяют вам контролировать то, что происходит, когда процесс открывает новый сокет. Они определены как тип BPF_PROG_TYPE_CGROUP_SOCK. Это полезно для обеспечения безопасности и контроля доступа к группам программ, которые могут открывать сокеты, не ограничивая возможности каждого процесса в отдельности.

Дополнительные программы для сокетов

Программы этого типа позволяют изменять параметры подключения к сокету во время выполнения, пока пакет проходит через несколько этапов в сетевом стеке ядра. Они привязаны к контрольным группам, во многом как BPF_PROG_TYPE_CGROUP_SOCK и BPF_PROG_TYPE_CGROUP_SKB, но, в отличие от них, могут вызываться несколько раз в течение жизненного цикла соединения. Эти программы определены как тип BPF_PROG_TYPE_SOCK_OPS.

Когда вы создаете такую программу BPF, вызов функции получает аргумент op, представляющий операцию, которую ядро собирается выполнить с соединением в сокете, следовательно, вы знаете, в какой момент программа вызывается в жизненном цикле соединения. Имея эту информацию, можно получить доступ к данным, например к сетевым IP-адресам и портам подключения, а также изменить параметры подключения, чтобы установить тайм-ауты и задать другое время задержки приема-передачи для данного пакета.

Например, Facebook использует такую методику, чтобы установить краткие сроки восстановления (RTO) для соединений в одном и том же центре данных. RTO — это время восстановления системы или сетевого соединения после сбоя. Оно также может дать представление о том, как долго система может быть недоступна, прежде чем все станет окончательно не таким, каким должно быть. В Facebook предполагается, что машины в одном центре обработки данных должны иметь короткое RTO, и Facebook изменяет этот порог с помощью программы BPF.

Программы карт в сокете

Программы BPF_PROG_TYPE_SK_SKB предоставляют вам доступ к картам сокетов и перенаправлениям сокетов. В следующей главе вы узнаете, как карты сокетов позволяют хранить ссылки на несколько сокетов. Имея эти ссылки, вы можете задействовать специальные помощники для перенаправления входящего пакета из одного сокета в другой. Это полезно, если вы хотите реализовать возможности балансировки нагрузки с помощью BPF. Отслеживая несколько сокетов, можно пересылать сетевые пакеты между ними, не покидая пространства ядра. Такие проекты, как Cillium и Facebook Katran (oreil.ly/wDtfR), широко используют программы данного типа для управления сетевым трафиком.

Программы для устройств контрольных групп

Программы данного типа позволяют решить, может ли операция в пределах контрольной группы быть выполнена для данного устройства. Эти программы имеют тип BPF_PROG_TYPE_CGROUP_DEVICE. Первая реализация cgroups (v1) имеет механизм, который позволяет вам устанавливать разрешения для определенных устройств, у второй версии контрольной группы такой возможности нет. Этот тип программ был разработан для обеспечения безопасности. В то же время возможность написать программу BPF дает вам больше свободы действий для установки таких разрешений, когда они нужны.

Программы доставки сообщений через сокет

Программы таких типов позволяют вам контролировать, должно ли доставляться сообщение, отправленное в сокет. Они относятся к BPF_PROG_TYPE_SK_MSG. Создав сокет, ядро сохраняет его в карте сокетов. Она обеспечивает ядру быстрый доступ к определенным группам сокетов. Когда вы с помощью сокетной программы сообщений BPF обращаетесь к карте сокетов, все сообщения, отправленные в них, отфильтровываются программой перед их доставкой. Перед фильтрацией сообщений ядро копирует данные, содержащиеся в сообщении, чтобы вы могли прочитать их и решить, что с ними делать. Эти программы имеют два возможных возвращаемых значения: SK_PASS и SK_DROP. Первое используется, если вы хотите, чтобы ядро отправило сообщение в сокет, а второе — если ядро игнорирует сообщение и не доставляет его в сокет, а просто отбрасывает.

Программы для доступа к необработанным точкам трассировки

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

Адресные программы сокетов контрольных групп

С помощью этих программ вы сможете манипулировать IP-адресами и номерами портов, к которым подключаются программы пользовательского пространства, когда ими управляют определенные контрольные группы. Бывают случаи, когда система использует несколько IP-адресов, если нужно, чтобы определенный набор программ пользовательского пространства задействовал один и тот же IP-адрес и порт (это прокси). Эти BPF-программы позволяют манипулировать привязками, помещая пользовательские программы в одну и ту же группу. При этом все входящие и исходящие соединения данных приложений используют IP-адрес и порт, которые предоставляет программа BPF. Такие программы определены с типом BPF_PROG_TYPE_CGROUP_SOCK_ADDR.

Сокетные программы повторного использования портов

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

Программы такого типа, как BPF_PROG_TYPE_SK_REUSEPORT, позволяют указать, будет ли ваша программа BPF повторно использовать порт. Вы можете запретить программам повторно задействовать один и тот же порт, если ваша BPF-программа возвращает SK_DROP, а также можете сообщить ядру, чтобы оно следовало собственной процедуре повторного применения, когда вы возвращаете SK_PASS в результате выполнения BPF-программ.

Программы разделения потока

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

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

Другие программы BPF

Мы говорили о типах программ, которые используются в разных средах, но стоит отметить, что есть еще несколько типов программ BPF, которые мы не рассмотрели. Здесь лишь упомянем их.

Программы классификатора трафика. BPF_PROG_TYPE_SCHED_CLS и BPF_PROG_TYPE_SCHED_ACT — два типа программ BPF, которые позволяют классифицировать сетевой трафик и изменять некоторые свойства пакетов в буфере сокета.

• Упрощенные туннельные программы. Программы BPF типов BPF_PROG_TYPE_LWT_IN, BPF_PROG_TYPE_LWT_OUT, BPF_PROG_TYPE_LWT_XMIT и BPF_PROG_TYPE_LWT_SEG6LOCAL позволяют вам связывать код с упрощенной туннельной инфраструктурой ядра.

• Программы инфракрасных устройств. Программы BPF_PROG_TYPE_LIRC_MODE2 позволяют использовать программы BPF в соединениях с инфракрасными устройствами, такими как удаленные контроллеры, просто для развлечения.

Все это специализированные программы, члены сообщества редко применяют их.

Далее поговорим о том, как BPF гарантирует, что ваши программы не вызовут катастрофического сбоя в системе после того, как ядро их загрузит. Это важная тема, ведь понимание того, как загружается программа, влияет на правильность ее написания.

Верификатор BPF

Позволять всем исполнять произвольный код внутри ядра Linux поначалу кажется ужасной идеей. Риск запуска программ BPF в производственных системах был бы слишком высоким, если бы не было верификатора BPF. Верификатор BPF — это тоже программа, работающая в вашей системе, и ее следует тщательно изучить, чтобы убедиться, что она хорошо выполняет свою работу. В последние годы исследователи безопасности обнаружили в верификаторе определенные уязвимости, которые позволяли злоумышленнику получать доступ к памяти в ядре, даже будучи непривилегированным пользователем. Вы можете подробнее прочитать об уязвимостях, подобных этой, в каталоге Common Vulnerabilities and Exposures (CVE) — списке известных тем безопасности, курируемом Министерством внутренней безопасности США. Например, CVE-2017-16995 описывает, как любой пользователь может читать из памяти ядра и записывать в нее, обходя верификатор BPF.

В этом разделе мы расскажем о мерах, предпринимаемых верификатором для предотвращения подобных проблем.

Первая проверка, выполняемая верификатором, — это статический анализ кода, который собирается загрузить виртуальная машина. Целью проверки является обеспечение ожидаемого завершения программы. Для этого верификатор создает прямой ациклический граф (DAG) с кодом. Каждая инструкция, которую проверяет верификатор, становится узлом графа, и каждый узел связан со следующей инструкцией. Сгенерировав этот граф, верификатор выполнит первый глубокий поиск (DFS), чтобы убедиться, что программа завершает работу и код не содержит опасных путей. Это означает, что он будет проходить каждую ветвь графа до конца, чтобы гарантировать отсутствие рекурсивных циклов.

Вот условия, при которых верификатор может отклонить ваш код в ходе данной проверки.

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

• Программа не пытается выполнить больше инструкций, чем максимально допустимо ядром. Сейчас максимальное количество выполняемых инструкций — 4096. Это ограничение должно предотвратить вечную работу BPF. В главе 3 мы обсудим, как безопасно использовать вложенные программы BPF, чтобы обойти его.

• Программа не содержит недоступных инструкций, таких как условия или функции, которые никогда не выполняются. Это предотвращает загрузку мертвого кода в ВМ и позволяет избежать задержки завершения программы BPF.

• Программа не пытается выйти за свои границы.

Вторая проверка, которую выполняет верификатор, — пробный запуск программы BPF. Это означает, что верификатор будет пытаться проанализировать каждую инструкцию, выполняемую программой, чтобы убедиться, что среди них нет недопустимых. При этом также проверяется, что все указатели памяти доступны и разыменованы правильно. Наконец, пробный прогон информирует верификатор о потоках управления в программе, чтобы гарантировать: независимо от того, какой путь управления выберет программа, она попадет в инструкцию BPF_EXIT. Верификатор отслеживает все пути, пройденные по ветвям в стеке, чтобы убедиться: при выборе нового пути программа не будет проходить его более одного раза. По окончании обеих проверок верификатор считает программу безопасной для выполнения.

Системный вызов bpf позволяет отлаживать проверки верификатора, если вам интересно посмотреть, как проходит анализ программ. Загружая программу с помощью следующего системного вызова, вы можете установить несколько атрибутов, которые позволят верификатору распечатать журнал своих действий:

union bpf_attr attr = {

  .prog_type = type,

  .insns     = ptr_to_u64(insns),

  .insn_cnt  = insn_cnt,

  .license   = ptr_to_u64(license),

  .log_buf   = ptr_to_u64(bpf_log_buf),

  .log_size  = LOG_BUF_SIZE,

  .log_level = 1,

};

 

bpf(BPF_PROG_LOAD, &attr, sizeof(attr));

Поле log_level сообщает верификатору, печатать ли какой-либо журнал. Журнал будет выведен, если вы установите значение поля равным 1, и ничего не будет выводиться, если значение поля — 0. Если вы хотите распечатать журнал верификатора, необходимо также указать буфер журнала и его размер. Этот буфер представляет собой несколько строк, которые вы можете распечатать, чтобы проверить решения, принятые верификатором.

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

В следующем разделе говорится, как BPF структурирует в памяти информацию программы. Зная, как структурирована программа, проще выяснить, как получить доступ к внутренним компонентам BPF, а также разобраться в отладке и понять поведение программ.

Формат типа BPF

Формат типа BPF (BTF) представляет собой набор структур метаданных, которые расширяют информацию для отладки программ, карт и функций BPF. BTF включает в себя сведения об источниках, поэтому такие инструменты, как BPFTool, о котором мы поговорим в главе 5, могут обеспечить богатую интерпретацию данных BPF. Эти метаданные хранятся в бинарной программе, в специальном разделе метаданных .BFT. Информация BTF помогает облегчить отладку программ, но значительно увеличивает размер двоичных файлов, потому что содержит информацию о типах всего, что объявлено в вашей программе. Верификатор BPF также использует эту информацию, чтобы убедиться, что типы структуры правильно определены вашей программой.

BTF применяется исключительно для аннотирования типов C. Компиляторы BPF, такие как LLVM, знают, как включить эту информацию, поэтому вам не нужно заниматься сложной задачей ее добавления в каждую структуру. Однако в некоторых случаях инструментальным средствам все еще нужны аннотации для расширения ваших программ. В последующих главах мы опишем, как эти аннотации вступают в игру и как инструменты наподобие BPFTool отображают данную информацию.

Оконечные вызовы BPF

Программы BPF могут вызывать другие программы BPF с помощью оконечных вызовов. Это мощная функция, поскольку она позволяет создавать более сложные программы, комбинируя меньшие функции BPF. Версии ядра до 5.2 имеют жесткое ограничение количества машинных инструкций, которые может генерировать программа BPF. Был установлен лимит 4096, чтобы гарантировать, что программы будут завершаться в течение разумного времени. Но поскольку люди создавали все более сложные программы BPF, им нужен был способ увеличить количество команд, ограниченное ядром, и именно здесь вступили в игру оконечные вызовы. Начиная с версии ядра 5.2, лимит инструкций увеличился до 1 млн. Вложенность оконечных вызовов в этом случае ограничена 32. Это означает, что вы можете объединить в цепочку до 32 программ, чтобы создать более сложное решение.

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

Резюме

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

В следующей главе еще немного углубимся в написание программ и приведем больше примеров. А также поговорим о том, как программы BPF общаются со своими визави в пространстве пользователя и обмениваются информацией.

Назад: 1. Введение
Дальше: 3. Карты BPF