Погружение в Linux ядро. Мой опыт с fanotify и eBPF
Как не стремись к автоматизации, всегда найдется какой-нибудь легаси сервис, который требует ручного обслуживания.
Был у меня такой сервис и работал он только тогда, когда все его файлы и каталоги принадлежали определенному пользователю.
ЧИТАТЬ ПЕРВЫМ В ТЕЛЕГРАМДоступ к сервису имели многие, поэтому люди порой троили и запускали команды в каталоге сервиса от root. Сервису было на это поебать, но до момента его перезапуска.
Обычно это чинилось очень легко, через chown -R
. Все это знали и никого это не смущало. Короче костыль ебаный.
Казалось бы, есть масса способов предотвратить такие ошибки: правильные права на файлы, ACL’ы, SELinux.
Но веселья в этом мало! Поэтому я решил заебенить свой собственный мониторинг файловой системы. Скиллов то предостаточно, хули нет.
Спойлер: Я залез в кроличью нору и знатно так хлебнул гавна.
Попытка намбер 1
В Linux есть API под названием fanotify, с его помощью можно получать события о действиях с файлами в юзерспейс.
Всё просто: инициализируем fanotify_init, указываем каталоги через fanotify_mark и читаем события из дескриптора.
Но тут же вылез огромный хуй:
- нельзя отслеживать каталоги рекурсивно (только целый маунт)
anotify
даёт только PID процесса, который что-то сделал. А чтобы узнать UID/GID — нужно лезть в/proc/<pid>/status
. То есть на каждое событие приходится открывать и парсить файлы в/proc
.
Решение вполне рабочее, но громоздкое. Я такие не люблю, этож думать надо, вайбкодингом не решается.
Попытка намбер 2
Вспоминаем что есть eBPF
. Это штука позволяет запускать программы прямо в ядре Linux. Они компилируются в байткод, проходят проверку, а потом гоняются через JIT почти с нативной скоростью.
Что такое eBPF можешь почитать тут и тут.
В eBPF
заебись то, что можно цепляться за разные функции ядра. Например, можно подцепиться к vfs_mkdir
или vfs_create
— это общий слой для работы с файлами.
То есть единая точка входа. Там можно отлавливать события и фильтровать их, не гоняя лишние переключения контекста.
Но и тут вылез хуй:
kprobes
на функции VFS нестабильны, в новых ядрах сигнатуры могут меняться или функции вообще исчезнуть.- фильтрацию приходится писать прямо в eBPF, а там свои ограничения. Нет бесконечных циклов, стек всего ~512 байт.
Да блядь…
Как я победил рекурсивный обход
Чтобы понять, что именно меняется в каталоге сервиса, пришлось использовать структуру dentry
и подниматься по дереву до родителя.
Но так как в eBPF нельзя сделать «бесконечный» цикл, я ограничил глубину с помощью MAX_DEPTH
.
На практике этого вполне достаточно. Глубина каталогов мне известна. Ну и конечно, пришлось аккуратно работать с RCU-локами, чтобы дерево не поменялось в момент обхода.
Кусок кода в первом комментарии, сюда сука снова не влез.
static bool is_monitored_dir(struct dentry *dentry, __u64 target_ino) {
bpf_rcu_read_lock();
struct dentry *curr_dentry = BPF_CORE_READ(dentry, d_parent);
struct inode *curr_inode;
__u64 curr_ino;
bool result = false;
#pragma unroll
for(int i=0; i < MAX_DEPTH; i++) {
if (!curr_dentry) {
break;
}
curr_inode = BPF_CORE_READ(curr_dentry, d_inode);
curr_ino = BPF_CORE_READ(curr_inode, i_ino);
if (curr_ino == target_ino) {
result = true;
break;
}
struct dentry *parent_dentry = BPF_CORE_READ(curr_dentry, d_parent);
if (curr_dentry == parent_dentry) {
break; // curr_dentry is its own root, we have reached the top of
// the tree.
}
curr_dentry = parent_dentry;
}
bpf_rcu_read_unlock();
return result;
}
Как можно улучшить
В идеале использовать не VFS-хуки, а LSM hooks (Linux Security Module).
Они стабильнее, понятнее и позволяют сразу работать с путями. Там можно красиво получить path и сразу преобразовать его в строку, а потом делать поиск подстроки.
Но в моём ядре этих хуков не было, хуй знает почему, видимо дистрибутив слишком древний. Надо попробовать на новых, чем черт не шутит.
Итоги
Эта поделка как и предполагалась, погрузила меня в печаль, душевные страдания, НО стала отличным тренажером для прокачки:
- Внутренностей Linux ядра
- Работы с eBPF
- И кучу другого с kernel-space
eBPF — мощнейший инструмент, но очень тонкий. Ошибёшься — будешь выебан в жопу.
Информации по нему много, но вся она разбросана. Собрать всё это в кучу было отдельным квестом.
Мораль?
Иногда самое простое решение — это chown -R
. Но куда интереснее — написать свой велосипед и заглянуть в кроличью нору Linux ядра.