В ядре FreeBSD 14.4 нашли серьёзную уязвимость — переполнение буфера на стеке. Проблема в функции kern_setcred_copyin_supp_groups(). Аргумент groups объявлен как gid_t ** — двойной указатель. Из-за этого sizeof(*groups) возвращает 8 байт (размер указателя на LP64), хотя разработчики ожидали 4 байта (sizeof(gid_t)). Этот sizeof используется при выделении памяти и при copyin. На стековом пути (когда групп меньше 16) буфер smallgroups занимает всего 64 байта (16 * 4). Но copyin копирует sc_supp_groups_nb * 8 байт. При максимальном значении 15 в стековый путь записывается 120 байт — переполнение на 60 байт за конец массива. Данные полностью контролируются атакующим из пользовательского пространства.
Хуже того: переполнение происходит до проверки привилегий. Вызов setcred(SETCREDF_SUPP_GROUPS, ...) любой локальный пользователь может отправить до того, как ядро проверит PRIV_CRED_SETCRED. Атакующий получает управление над содержимым стека.
Эксплойт работает в два варианта. Без SMAP/SMEP атакующий перезаписывает сохранённые регистры в прологе user_setcred(), включая r12. Этот r12 всплывает через sys_setcred() в amd64_syscall(), где он используется как указатель на td_proc. Двухуровневый косвенный вызов полностью под контролем: атакующий подставляет фейковый struct sysentvec, чей слот sv_set_syscall_retval (смещение 0xc8) ведёт на шелл-код в userspace. Шелл-код обнуляет UID в реальном td_ucred — и процесс получает root.
С включёнными SMAP/SMEP и без утечки информации нужен другой подход. Косвенный вызов в amd64_syscall кладёт в rcx произвольное 8-байтовое значение (K1). Если найти гаджет, который пишет rcx + 1 в td->td_ucred, можно подменить указатель на креды. Такой гаджет живёт в zfs.ko, внутри ZSTD_initCStream_advanced. Он выполняет mov qword ptr [rdi + 0x180], rax, где rax = rcx + 1. После этого текущий поток получает фейковый ucred.
Фейковый ucred размещается через setproctitle(2). Ядро выделяет 256-байтовый слот в зоне PARGS и копирует туда до 244 байт. Атакующий заполняет поля так, чтобы cr_uid = 0, cr_prison = &prison0 и т.д. Но для K1 нужен адрес P_base - 1. Проблема: в P_base - 1 есть нулевой байт на смещении 4, который обрезает strlcpy при вызове thr_set_name. Решение — форкнуть дочерний процесс, который сделает свой setproctitle со значением P_base - 1 на смещении 0xd0. Тогда цепочка выставляет r12 = C_base + 0xd0 - 0x3f8 и считывает K1 оттуда.
Символы ядра (адрес ZSTD_initCStream_advanced и prison0) эксплойт резолвит через непривилегированные kldnext(2) и kldsym(2). Один бинарник работает на всех версиях 14.4 patchset. После успешной эксплуатации поток получает эффективный root для VFS-операций — остаётся только установить постоянство или запустить привилегированную оболочку.