В ядре Linux устранена следующая уязвимость:
bpf: исправлена гонка в devmap на PREEMPT_RT. В ядрах PREEMPT_RT xdp_dev_bulk_queue (bq) для каждого процессора может быть
доступ к ним осуществляется одновременно несколькими вытесняемыми задачами на одном и том же ЦП. Исходный код предполагает, что bq_enqueue() и __dev_flush() выполняются атомарно.
относительно друг друга на одном и том же процессоре, полагаясь на
local_bh_disable() для предотвращения вытеснения.
Однако в PREEMPT_RT
local_bh_disable() вызывает миграцию_disable() только (когда
PREEMPT_RT_NEEDS_BH_LOCK не установлен) и не отключает
вытеснение, которое позволяет планированию CFS вытеснять задачу во время
bq_xmit_all(), позволяющий войти другой задаче на том же процессоре
bq_enqueue() и одновременно работать с одним и тем же bq для каждого процессора. Это приводит к нескольким гонкам:
1. Двойное освобождение/использование после освобождения на bq->q[]: снимки bq_xmit_all()
cnt = bq->count, затем выполняет итерацию bq->q[0..cnt-1] для передачи кадров.
Если вытеснение происходит после моментального снимка, вторая задача может вызвать bq_enqueue().
-> bq_xmit_all() на том же bq, передавая (и освобождая)
те же кадры. Когда первая задача возобновляется, она работает на устаревшем
указатели в bq->q[], вызывающие использование после освобождения.
2. Повреждение bq->count и bq->q[]: одновременное изменение bq_enqueue()
bq->count и bq->q[], пока bq_xmit_all() их читает.
3.
Гонка по удалению dev_rx/xdp_prog: __dev_flush() очищает bq->dev_rx и
bq->xdp_prog после bq_xmit_all(). Если вытеснено между
bq_xmit_all() возвращает и bq->dev_rx = NULL, вытесняющее действие.
bq_enqueue() видит, что dev_rx все еще установлен (не NULL), пропускает добавление bq в
flash_list и ставит кадр в очередь. Когда __dev_flush() возобновит работу,
он очищает dev_rx и удаляет bq из flash_list, делая осиротевшим
недавно поставленный в очередь кадр.
4. __list_del_clearprev() наlush_node: аналогично гонке cpumap,
обе задачи могут вызывать __list_del_clearprev() на одном и том жеlush_node,
второй разыменовывает указатель prev, уже установленный в NULL.
Гонка между задачей А (__dev_flush -> bq_xmit_all) и задачей Б
(bq_enqueue -> bq_xmit_all) на том же процессоре:
Задача A (xdp_do_flush) Задача B (перенаправление ndo_xdp_xmit)
---------------------- --------------------------------
__dev_flush(flush_list)
bq_xmit_all (бк)
cnt = bq->count /* например. 16 */
/* начинаем итерацию bq->q[] */
<-- CFS вытесняет задачу A -->
bq_enqueue (dev, xdpf)
bq->count == DEV_MAP_BULK_SIZE
bq_xmit_all (bq, 0)
cnt = bq->count /* те же 16! */
ndo_xdp_xmit(bq->q[])
/* кадры, освобожденные драйвером */
бк->счет = 0
<-- Задача A возобновляется -->
ndo_xdp_xmit(bq->q[])
/* use-after-free: кадры уже освобождены! */
Исправьте это, добавив local_lock_t в xdp_dev_bulk_queue и получив
это в bq_enqueue() и __dev_flush(). Эти пути уже проходят под
local_bh_disable(), поэтому используйте local_lock_nested_bh(), который в не-RT
чистая аннотация без накладных расходов, а PREEMPT_RT обеспечивает
Спящая блокировка для каждого процессора, которая сериализует доступ к bq.
Показать оригинальное описание (EN)
In the Linux kernel, the following vulnerability has been resolved: bpf: Fix race in devmap on PREEMPT_RT On PREEMPT_RT kernels, the per-CPU xdp_dev_bulk_queue (bq) can be accessed concurrently by multiple preemptible tasks on the same CPU. The original code assumes bq_enqueue() and __dev_flush() run atomically with respect to each other on the same CPU, relying on local_bh_disable() to prevent preemption. However, on PREEMPT_RT, local_bh_disable() only calls migrate_disable() (when PREEMPT_RT_NEEDS_BH_LOCK is not set) and does not disable preemption, which allows CFS scheduling to preempt a task during bq_xmit_all(), enabling another task on the same CPU to enter bq_enqueue() and operate on the same per-CPU bq concurrently. This leads to several races: 1. Double-free / use-after-free on bq->q[]: bq_xmit_all() snapshots cnt = bq->count, then iterates bq->q[0..cnt-1] to transmit frames. If preempted after the snapshot, a second task can call bq_enqueue() -> bq_xmit_all() on the same bq, transmitting (and freeing) the same frames. When the first task resumes, it operates on stale pointers in bq->q[], causing use-after-free. 2. bq->count and bq->q[] corruption: concurrent bq_enqueue() modifying bq->count and bq->q[] while bq_xmit_all() is reading them. 3. dev_rx/xdp_prog teardown race: __dev_flush() clears bq->dev_rx and bq->xdp_prog after bq_xmit_all(). If preempted between bq_xmit_all() return and bq->dev_rx = NULL, a preempting bq_enqueue() sees dev_rx still set (non-NULL), skips adding bq to the flush_list, and enqueues a frame. When __dev_flush() resumes, it clears dev_rx and removes bq from the flush_list, orphaning the newly enqueued frame. 4. __list_del_clearprev() on flush_node: similar to the cpumap race, both tasks can call __list_del_clearprev() on the same flush_node, the second dereferences the prev pointer already set to NULL. The race between task A (__dev_flush -> bq_xmit_all) and task B (bq_enqueue -> bq_xmit_all) on the same CPU: Task A (xdp_do_flush) Task B (ndo_xdp_xmit redirect) ---------------------- -------------------------------- __dev_flush(flush_list) bq_xmit_all(bq) cnt = bq->count /* e.g. 16 */ /* start iterating bq->q[] */ <-- CFS preempts Task A --> bq_enqueue(dev, xdpf) bq->count == DEV_MAP_BULK_SIZE bq_xmit_all(bq, 0) cnt = bq->count /* same 16! */ ndo_xdp_xmit(bq->q[]) /* frames freed by driver */ bq->count = 0 <-- Task A resumes --> ndo_xdp_xmit(bq->q[]) /* use-after-free: frames already freed! */ Fix this by adding a local_lock_t to xdp_dev_bulk_queue and acquiring it in bq_enqueue() and __dev_flush(). These paths already run under local_bh_disable(), so use local_lock_nested_bh() which on non-RT is a pure annotation with no overhead, and on PREEMPT_RT provides a per-CPU sleeping lock that serializes access to the bq.
Характеристики атаки
Последствия
Строка CVSS v3.1