RDMA implementation review using AI (Claude 4.7) #69

Closed
opened 2026-05-17 03:40:44 +03:00 by maxpain · 1 comment

Аудит реализации RDMA/RDMACM в Vitastor

Дата проведения: 2026-05-16
Версия кода: ветка master, последний коммит e136a1df
Область аудита:

  • src/client/msgr_rdma.{h,cpp} — основной RDMA-мессенджер (Verbs)
  • src/client/msgr_rdmacm.cpp — RDMA-CM (Connection Manager) реализация
  • src/client/messenger.{h,cpp} — общий мессенджер, инициализация
  • src/client/msgr_receive.cpp, msgr_send.cpp, msgr_stop.cpp — путь данных
  • src/nfs/nfs_proxy_rdma.cpp — NFS-RDMA сервер (RFC 5666 / 8166)
  • src/nfs/rdma_alloc.{h,cpp} — простой RDMA-aware аллокатор
  • src/osd/osd.cpp, src/osd/osd_secondary.cpp, src/osd/osd_cluster.cpp — интеграция в OSD

Методология: Файлы прочитаны целиком (не grep-cherry-pick). Анализ выполнен пятью параллельными агентами по областям: жизненный цикл и cleanup, обработка ошибок, корректность протокола, безопасность/границы, конкурентность/конфигурация. Все находки независимо проверены чтением исходников.

Итог: Найдено >50 проблем. Реализация RDMA в Vitastor функционально работает в «хорошую погоду» (RoCEv2, x86_64, надёжная сеть, доверенные пиры), но имеет множественные критические дефекты, ломающие надёжность, безопасность и производительность в реальной эксплуатации. Особое внимание разработчика заслуживают: систематическое использование exit(1) на любых временных ошибках RDMA; use-after-free в NFS RDMA-сервере; целочисленные переполнения с потенциально эксплуатируемыми последствиями в NFS-RPC-RDMA; систематически некорректные порядки уничтожения объектов RDMA.


1. КРИТИЧЕСКИЕ ПРОБЛЕМЫ (Critical) — требуют немедленного исправления

1.1. Системное использование exit(1) при любой ошибке RDMA — один битый клиент роняет весь процесс

Файлы и места:

  • src/client/msgr_rdma.cpp:566 — try_send_rdma_wr после ibv_post_send
  • src/client/msgr_rdma.cpp:603 — try_send_rdma при ibv_reg_mr
  • src/client/msgr_rdma.cpp:657 — try_recv_rdma_wr после ibv_post_recv
  • src/client/msgr_rdma.cpp:670 — init_recv_rdma при ibv_reg_mr
  • src/client/msgr_rdma.cpp:697 — handle_rdma_events при ibv_req_notify_cq
  • src/client/msgr_rdmacm.cpp:84, :99 — handle_rdmacm_events при ошибках rdma_get_cm_event/rdma_ack_cm_event
  • src/nfs/nfs_proxy_rdma.cpp:474, :500, :759, :783, :1048 — все аналогичные места в NFS-RDMA
  • src/nfs/rdma_alloc.cpp:118 — rdma_malloc_alloc при ibv_reg_mr

Описание: В коде около 13 точек, где временная или ожидаемая ошибка вызовов libibverbs/librdmacm приводит к exit(1) всего процесса. В частности:

  • ibv_post_send / ibv_post_recv возвращают ошибку, если QP перешла в состояние IBV_QPS_ERR (а это происходит автоматически при любой ошибке транспортного уровня — RNR retry exceeded, transport timeout, length error и т.п.). Один сломанный клиент → весь OSD падает.
  • ibv_reg_mr падает при превышении ulimit -l memlock (часто 64 МБ на дефолтных дистрибутивах) или при достижении dev_attr.max_mr HCA. Достаточно открыть много соединений, чтобы убить OSD.
  • ibv_req_notify_cq фактически возвращает ошибку, только если CQ невалидна — что возможно, например, при асинхронном IBV_EVENT_DEVICE_FATAL.

Серьёзность: Critical — практически любой производственный сценарий приводит к каскадному падению OSD.

Сценарии срабатывания:

  1. Клиент с глючным железом доводит свою QP до состояния ERROR. Сервер на следующей отправке завершает работу.
  2. Узлу не хватает memlock-памяти (или достигнут лимит max_mr) при росте числа подключений — узел падает.
  3. Hot-plug удалил RDMA-устройство в виртуальной/контейнерной среде.

Рекомендация:

  • Заменить все exit(1) в RDMA/RDMACM-путях на:
    • для проблем с конкретным клиентом — stop_client(client_id);
    • для проблем с контекстом устройства — пометить контекст невалидным, остановить всех клиентов на нём, отключить RDMA, не убивать процесс.
  • Переименовать init_recv_rdma так, чтобы возвращаемое bool действительно использовалось; сейчас функция всегда возвращает true, но при ошибке убивает процесс.
  • Аналогично rdma_malloc_alloc должен возвращать NULL, а не падать.

1.2. Use-after-free в NFS RDMA handle_io при ошибке WC

Файл: src/nfs/nfs_proxy_rdma.cpp:792-815

if (wc[i].status != IBV_WC_SUCCESS) {
    fprintf(stderr, "RDMA work request failed ...\n");
    conn->client->stop();
    // but continue to handle events to purge the queue
}
if (wc[i].wr_id == 1) {
    auto b = conn->recv_buffers[conn->next_recv_buf];
    conn->recv_buffers.erase(...);
    if (conn->cur_credit > 0 && !conn->client->stopped) { ... }
}

Описание: При получении WC со статусом ошибки вызывается conn->client->stop(). Внутри nfs_client_t::stop() (см. nfs_proxy.cpp) после установки stopped=true при refs <= 0 происходит немедленное delete this. delete client вызывает client->destroy_rdma_conn() → delete rdma_conn → ~nfs_rdma_conn_t() → уничтожает сам conn, по которому мы сейчас итерируем в handle_io().

Код после stop() продолжает обращаться к conn->client->stopped, conn->recv_buffers, conn->outbox — это чтение из освобождённой памяти.

Серьёзность: Critical — гарантированный краш или повреждение памяти при любом сетевом сбое NFS-RDMA-клиента.

Рекомендация: Использовать паттерн отложенного удаления, как сделано в OSD (destroy_client через set_immediate_ops / refs-механику). После client->stop() — break из цикла обработки WC и не трогать conn.


1.3. Ассерты в ~nfs_rdma_conn_t() приводят к abort() при разрыве клиента в момент работы

Файл: src/nfs/nfs_proxy_rdma.cpp:413-417

nfs_rdma_conn_t::~nfs_rdma_conn_t() {
    assert(!outbox.size());
    assert(!chunk_inbox.size());
    assert(!chunk_read_postponed.size());
    ...
}

Описание: Деструктор требует, чтобы все три очереди были пусты. Однако стандартный сценарий разрыва:

  1. Клиент отправил RPC через RDMA.
  2. Сервер вызвал post_chunk_reads(rop, true) → chunk_inbox.push_back(rop) (или поместил в outbox/chunk_read_postponed).
  3. Сеть рвётся / клиент уходит → приходит RDMA_CM_EVENT_DISCONNECTED.
  4. handle_rdmacm_events → stop_clients.push_back(client) → client->stop() → delete rdma_conn → assert срабатывает → abort().

Серьёзность: Critical — любой разрыв клиента в момент незавершённой RDMA-операции крашит NFS-прокси целиком.

Рекомендация:

  • В деструкторе (или, лучше, в nfs_client_t::stop до delete rdma_conn) очищать все три очереди, корректно освобождая rop->buffer через rdma_malloc_free и сам rop через free/xdr_free.
  • Только после очистки — delete rdma_conn.

1.4. rdma_malloc_destroy вызывает abort() при оставшихся живых фрагментах

Файл: src/nfs/rdma_alloc.cpp:73-77

if (frag_it->second.len != frag_it->second.rgn->len) {
    if (force) {
        fprintf(stderr, "BUG: Attempt to destroy RDMA allocator while buffers are not freed yet\n");
        abort();
    }
    break;
}

Описание: При уничтожении nfs_rdma_dev_state_t зовётся rdma_malloc_destroy. Если в аллокаторе остались живые фрагменты (а они там почти всегда есть при аварийном разрыве — rop->buffer, read-chunk-буферы), вызывается abort().

Серьёзность: Critical — крах NFS-прокси при разрыве клиента в момент работы.

Связано с: 1.3, 1.5.

Рекомендация: Снять abort() — заменить на лог + утечку (лучше), либо предварительно гарантированно очистить все фрагменты в вышестоящем коде. Самый чистый вариант: владение алокатором и QP должны быть в одних руках; перед уничтожением одного — чистить второе.


1.5. Неверный порядок уничтожения в ~nfs_rdma_conn_t(): CQ удаляется ДО QP

Файл: src/nfs/nfs_proxy_rdma.cpp:418-438

nfs_rdma_conn_t::~nfs_rdma_conn_t() {
    ...
    for (auto & b: recv_buffers) { ibv_dereg_mr(b.mr); free(b.buf); }
    ...
    if (conn_dev) {
        ...
        delete conn_dev;         // <-- 1. уничтожает CQ + channel + alloc
        conn_dev = NULL;
    }
    if (id) {
        ctx->rdma_connections.erase(id);
        if (id->qp) {
            rdma_destroy_qp(id); // <-- 2. уничтожает QP ПОСЛЕ CQ
        }
        rdma_destroy_id(id);
    }
}

Описание: delete conn_dev вызывает ibv_destroy_cq(cq). По стандарту libibverbs, ibv_destroy_cq возвращает EBUSY, пока на CQ ссылается живой QP. Здесь QP уничтожается уже после CQ — порядок неверен.

Серьёзность: Critical — приводит к утечке CQ/PD/channel + UB в зависимости от драйвера. На Mellanox обычно «работает», но даёт «висящие» CQ.

Рекомендация: Поменять порядок:

if (id && id->qp) rdma_destroy_qp(id);    // сначала QP
if (conn_dev) { delete conn_dev; ... }    // потом CQ/PD
if (id) rdma_destroy_id(id);              // потом ID

Параллельно см. 1.3, 1.4.


1.6. Use-after-free в handle_rdmacm_events при множественных событиях для одного cmid

Файлы:

  • src/client/msgr_rdmacm.cpp:73-167
  • src/nfs/nfs_proxy_rdma.cpp:245-310

Описание: Чтобы обойти зависание rdma_destroy_id, код копирует события в events_copy и акает их сразу. Это решает первоначальную проблему, но создаёт новую: если за один poll-цикл придёт несколько событий для одного cmid (например RDMA_CM_EVENT_DISCONNECTED + RDMA_CM_EVENT_TIMEWAIT_EXIT), первое событие приведёт к stop_client(...) → delete rdma_conn → rdma_destroy_id(cmid). Второе событие в events_copy ссылается на уже уничтоженный cmid → use-after-free при rdmacm_connections.find(ev->id) (как ключе в hashmapе), а также при последующих fprintf(... "...event for client %ju", cli_it->second->client_id) и т.д.

Дополнительно: события, пришедшие в канал после дренажа в текущем цикле, но до реального вызова rdma_destroy_id, всё ещё могут привести к зависанию rdma_destroy_id — обход неполон.

Серьёзность: Critical — реальный риск падения при flapping-соединениях.

Рекомендация:

  • В events_copy сохранять не сам rdma_cm_event, а минимальные нужные поля (event, id, status). Перед использованием id проверять валидность через rdmacm_connections.find(...) и rdmacm_connecting.find(...).
  • Аккуратнее: перед rdma_destroy_id ещё раз дренировать канал на наличие событий для этого id.
  • Лучше всего: отложить rdma_destroy_id до начала следующего вызова handle_rdmacm_events через специальную «корзину для уничтожения».

1.7. Потенциальный stack buffer overflow в NFS RPC-RDMA через переполнение wr_count

Файл: src/nfs/nfs_proxy_rdma.cpp:588-655

xdr_write_chunk *reply_chunk = rop->in_rdma_msg.rdma_body.rdma_msg.rdma_reply;
int reply_chunk_wr_count = (reply_chunk ? reply_chunk->target.target_len : 0);
uint32_t wr_count = 1 + (chunk_iov ? 1 : 0) + (reply_chunk ? reply_chunk_wr_count : 0);
if (wr_count > ctx->max_send_wr) { ... ERR_CHUNK ... }
...
ibv_sge sges[wr_count];      // VLA на стеке
ibv_send_wr wrs[wr_count];   // VLA на стеке
...
for (uint32_t i = 0; i < reply_chunk->target.target_len && pos < msg_size; i++) {
    sges[wr_pos] = {...};
    wrs[wr_pos] = ...;
    wr_pos++;
}

Описание: reply_chunk->target.target_len — это u_int (unsigned int), пришедший с провода от клиента. Если, например, target_len = 0xFFFFFFFE:

  • reply_chunk_wr_count = 0xFFFFFFFE (int, но интерпретируется в uint32);
  • wr_count = 1 + 1 + 0xFFFFFFFE = 0 (переполнение uint32);
  • проверка wr_count > ctx->max_send_wr пройдёт (0 < 1024);
  • VLA sges[0]/wrs[0] (или 1-2) на стеке;
  • цикл по target_len = 0xFFFFFFFE итераций пишет в sges[wr_pos++] далеко за пределами VLA → переполнение стека.

Аналогично — xdr_xdr_read_list / xdr_xdr_write_list декодеры (src/nfs/proto/rpc_rdma_xdr.cpp:34-42, 54-62) рекурсивно, без верхнего лимита, спускаются по связному списку — при длинном входном списке вызывают stack overflow при декодировании (классический DoS через malformed packet).

Серьёзность: Critical — потенциально удалённо эксплуатируемое условие. CVE-уровень.

Рекомендация:

  • Жёстко лимитировать target_len сразу после XDR-декодирования (например, ≤ 64).
  • Использовать __builtin_add_overflow для расчёта wr_count.
  • Использовать std::vector или фиксированный размер вместо VLA.
  • Переписать XDR-декодеры xdr_xdr_read_list/xdr_xdr_write_list итеративно с лимитом длины.

1.8. Целочисленное переполнение в rdma_malloc_alloc ведёт к heap-overflow

Файл: src/nfs/rdma_alloc.cpp:104-145, в связке с src/nfs/nfs_proxy_rdma.cpp:988-1054

size_t alloc_size = ((size + self->rdma_alloc_size - 1) / self->rdma_alloc_size) * self->rdma_alloc_size;

Описание: В nfs_rdma_conn_t::post_chunk_reads read_chunk_size накапливается из недоверенных значений cur->entry.target.length (uint32_t). С учётом существующей проверки read_chunk_count > max_rdma_reads (≤ 255), теоретически возможна сумма около 255 * 4 ГБ ≈ 1 ТБ. На 64-битной системе это не превышает SIZE_MAX, но при округлении вверх:

size_t alloc_size = ((size + rdma_alloc_size - 1) / rdma_alloc_size) * rdma_alloc_size;

если size ≈ SIZE_MAX - 100, то size + rdma_alloc_size - 1 переполняется, давая маленькое значение → malloc_or_die(маленький размер) + ibv_reg_mr на «настоящий размер» (но MR регистрируется на тот же r->len = alloc_size_HUGE, что и в malloc-параметре). Далее pos += cur->entry.target.length пишет байты RDMA-Read'ом за пределами выделенного буфера → heap overflow.

Серьёзность: Critical (если достижимо через малых-чанков-много, либо одного большого) — потенциальный RCE через heap-corruption.

Рекомендация:

  • Проверять __builtin_add_overflow(size, rdma_alloc_size - 1, &intermediate) перед делением/умножением.
  • Установить верхний лимит read_chunk_size (например max_send_wr * 256 КБ = 256 МБ).
  • Сделать аналогичные проверки везде, где сумма пользовательских length идёт в malloc.

1.9. Использование lrand48() для PSN без srand48() — предсказуемые PSN

Файл: src/client/msgr_rdma.cpp:443

conn->addr.psn = lrand48() & 0xffffff;

Описание: srand48() нигде в OSD/RDMA-коде не вызывается, поэтому lrand48() инициализирована константным сидом 1 (POSIX), и PSN-последовательность одинакова между всеми OSD и сохраняется между перезапусками.

PSN (Packet Sequence Number) в RC-QP — единственная не-инвариантная по адресу защита от стейл/спуф-пакетов. На предсказуемых PSN атакующий с доступом к сети способен инжектить «правильные» пакеты в установленное RDMA-соединение.

Серьёзность: Critical (модель угроз "atomic untrusted network"). Для типичного дата-центра, где RDMA-сеть считается доверенной, проблема меньше — но и в этом случае стейл-пакеты предсказуемы и могут запутывать диагностику.

Рекомендация: Перед первым lrand48() инициализировать srand48(...) от clock_gettime(CLOCK_MONOTONIC)+pid или, лучше, использовать getrandom() напрямую.


1.10. Неверный порядок в ~msgr_rdma_buf_t(): free(buf) до ibv_dereg_mr(mr)

Файл: src/client/msgr_rdma.cpp:68-80

msgr_rdma_buf_t::~msgr_rdma_buf_t() {
    if (buf) { free(buf); buf = NULL; }
    if (mr) { ibv_dereg_mr(mr); mr = NULL; }
}

Описание: По спецификации libibverbs, ibv_dereg_mr должно вызываться до освобождения подложенной памяти. После free(buf), hardware ещё может иметь outstanding DMA, использующих MR, который ссылается на освобождённую память → UB / повреждение данных / crash на некоторых драйверах. На практике, к моменту попадания сюда QP уже уничтожен, и DMA должно быть завершено, но порядок остаётся UB и небезопасным паттерном.

Серьёзность: High–Critical (зависит от драйвера). На Mellanox обычно «работает», на iWARP может крашить.

Рекомендация: Поменять порядок: сначала ibv_dereg_mr(mr), затем free(buf). Это однострочное исправление.


1.11. Use-after-free на ibv_context в ~osd_messenger_t() для is_cm-контекстов

Файлы: src/client/messenger.cpp:264-299, в связке с src/client/msgr_rdma.cpp:56-66, src/client/msgr_rdmacm.cpp:169-203

Описание: При уничтожении мессенджера:

  1. Сначала идёт цикл stop_client(..., force_delete=true). Каждый клиент с RDMA-CM-соединением через ~msgr_rdma_connection_t() вызывает rdma_destroy_qp(cmid) + rdma_destroy_id(cmid). rdma_destroy_id потенциально освобождает ibv_context, на который ссылается общий msgr_rdma_context_t (is_cm=true).
  2. Затем идёт цикл по rdma_contexts, который вызывает delete rdma_context → ~msgr_rdma_context_t() → ibv_destroy_cq, ibv_destroy_comp_channel, ibv_dealloc_pd поверх уже потенциально мёртвого ibv_context.

В теории ibv_context рефкаунтится внутренне (CQ/PD держат ссылку), и пока их не уничтожить, контекст жив. Однако cm_refs в коде помечен как FIXME now unused (см. msgr_rdma.h:41, msgr_rdmacm.cpp:482) — поле инкрементируется и декрементируется, но никогда не проверяется. То есть в ~osd_messenger_t() нет проверки, что для CM-контекста все QP действительно уничтожены.

Серьёзность: Critical (если рассинхрон порядка деструкции произойдёт).

Рекомендация:

  • Довести до конца cm_refs: уничтожать is_cm msgr_rdma_context_t только когда cm_refs == 0 И ни один rdmacm_connecting_t его не держит.
  • Учитывать в cm_refs также rdmacm_connecting_t (+1 в rdmacm_address_resolved, -1 в rdmacm_on_connect_peer_error/rdmacm_established).

1.12. Утечка rdmacm_connecting_t при разрушении мессенджера

Файл: src/client/messenger.cpp:264-299 (деструктор)

Описание: ~osd_messenger_t() останавливает клиентов (stop_client), уничтожает контексты, уничтожает rdmacm_evch, но никогда не итерирует по rdmacm_connecting. Незавершённые соединения остаются — это cmid + активные таймеры. Когда таймер сработает позже, лямбда захватывает this (мёртвый osd_messenger_t) → segfault.

Серьёзность: Critical (при тестах/рестартах).

Рекомендация: Перед rdma_destroy_event_channel(rdmacm_evch) пройтись по rdmacm_connecting, очистить таймеры, уничтожить cmid, удалить структуры.


1.13. Race в rdmacm_set_conn_timeout: callback может ссылаться на удалённый cmid

Файл: src/client/msgr_rdmacm.cpp:301-317

conn->timeout_id = tfd->set_timer(1000*peer_connect_timeout, false, [this, cmid = conn->cmid](int timer_id) {
    auto conn = rdmacm_connecting.at(cmid);   // <-- может бросить out_of_range
    ...
});

Описание: Лямбда захватывает raw-pointer cmid. Если до срабатывания таймера соединение перешло в установленное состояние или было удалено через rdmacm_on_connect_peer_error, rdmacm_connecting.at(cmid) бросит out_of_range — необработанное исключение → std::terminate → abort.

tfd->clear_timer(conn->timeout_id) пытается отменить таймер, но если callback уже встал в очередь обработки и clear_timer срабатывает в окне между запуском и фактическим execution — поведение зависит от реализации timerfd_manager_t.

Серьёзность: Critical (зависит от семантики clear_timer).

Рекомендация:

  • Внутри callback'а использовать find, не at. Если не нашли — return.
  • Лучше: вместо raw-pointer cmid использовать generation-counter или захватывать weak_ptr на conn.

2. ВЫСОКОПРИОРИТЕТНЫЕ ПРОБЛЕМЫ (High)

2.1. Отсутствие обработки асинхронных событий устройств — ibv_get_async_event не используется НИГДЕ

Файлы: весь RDMA-код

Описание: В Vitastor нет обращений к ibv_get_async_event / ibv_ack_async_event. Это значит, что игнорируются критические события устройства:

  • IBV_EVENT_DEVICE_FATAL — фатальная ошибка HCA
  • IBV_EVENT_PORT_ERR / IBV_EVENT_PORT_ACTIVE — состояние порта (вышел/восстановился)
  • IBV_EVENT_LID_CHANGE, IBV_EVENT_GID_CHANGE — переконфигурация subnet
  • IBV_EVENT_CLIENT_REREGISTER — переподпись MR требуется
  • IBV_EVENT_QP_FATAL, IBV_EVENT_QP_REQ_ERR, IBV_EVENT_QP_ACCESS_ERR — фатальные ошибки QP
  • IBV_EVENT_CQ_ERR, IBV_EVENT_SRQ_ERR — фатальные ошибки CQ/SRQ

Сценарии:

  • Порт IB ушёл в ERR (отсоединён кабель). Кластер заметит проблему только через TCP-keepalive (через десятки секунд) или истечение retry_cnt в RC-QP (≈ минуты).
  • Hot-plug RDMA-устройства в виртуальной среде → процесс висит, считая соединения живыми.
  • LID-change на IB-фабрике → новые соединения не устанавливаются, диагностики нет.

Серьёзность: High — критично для production. Долгоживущие OSD не способны корректно реагировать на изменения состояния сети/устройства.

Рекомендация: Открыть ibv_context->async_fd (он уже non-blocking), повесить epoll-handler, в нём вызывать ibv_get_async_event + ibv_ack_async_event, и обрабатывать события:

  • Fatal device/port — остановить все клиенты на этом контексте, попытаться переинициализировать.
  • LID/GID change — переподключить соединения.
  • QP/CQ errors — остановить соответствующих клиентов.

2.2. min_rnr_timer = 1 (10 микросекунд) — лавины RNR-retries

Файл: src/client/msgr_rdma.cpp:487

.min_rnr_timer = 1,
.timeout       = 14,
.retry_cnt     = 7,
.rnr_retry     = 7,

Описание: По IBA spec §11.4.1.1.5, min_rnr_timer — это прямой индекс таблицы, где 1 = 0.01 мс (10 мкс), 12 = 0.64 мс, 16 = 2.56 мс, 0 = 655.36 мс. Комментарий в коде «4.096us*2^(timeout+1)» относится к ack-timeout, не к min_rnr_timer — кодом ошибочно объединили формулы.

С min_rnr_timer = 1 (10 мкс), при возникновении RNR-NAK (Receiver Not Ready — приёмник не успел запостить RECV WR), отправитель ретраит через 10 мкс. Если приёмник под нагрузкой и не успевает за 10 мкс, лавина из rnr_retry = 7 попыток истощится за миллисекунды, после чего QP уйдёт в ERR. Отсюда IBV_WC_RNR_RETRY_EXC_ERR → stop_client (или, до исправления 1.1 — exit(1)).

Серьёзность: High — реальные сетевые/нагрузочные сценарии вызывают преждевременные разрывы.

Рекомендация: Установить min_rnr_timer = 12 (0.64 мс) минимально, лучше 16 (2.56 мс). Сделать настраиваемым через rdma_min_rnr_timer.

Дополнительно: rnr_retry = 7 — особое значение «бесконечные ретраи». Это значит, что при «висящем» приёмнике отправитель никогда не сдастся — IO залипает навсегда. Лучше использовать 6 (6 попыток) и полагаться на верхнеуровневые таймауты.


2.3. RDMA NOMSG не поддерживается — нарушение RFC 8166

Файл: src/nfs/nfs_proxy_rdma.cpp:895-916

if (rmsg.rdma_vers != 1 || rmsg.rdma_body.proc != RDMA_MSG) {
    // ERR_VERS / ERR_CHUNK
}

Описание: Принимается только RDMA_MSG. RDMA_NOMSG (RFC 8166 §3.7, тип сообщения, в котором payload передаётся целиком в read/reply-чанках, а SEND содержит только заголовок) не реализован. Linux NFS-клиент использует RDMA_NOMSG для крупных запросов с inline-частью больше inline_threshold.

Серьёзность: High — нарушение совместимости с RFC. Часть сценариев NFS-RDMA не работает с Vitastor.

Рекомендация: Добавить обработку RDMA_NOMSG: после получения SEND с типом NOMSG сделать RDMA_READ на reply chunk, и далее декодировать XDR.


2.4. NFS RDMA: утечка used_buffers при остановке клиента

Файл: src/nfs/nfs_proxy_rdma.cpp:481-484 (post_recv с проверкой stopped), :418-423 (деструктор по recv_buffers)

Описание: В post_recv есть if (client->stopped) return;. В ~nfs_rdma_conn_t() освобождаются буферы из recv_buffers. Но между ними буфер живёт в std::map<void*, nfs_rdma_buf_t> used_buffers (туда заносится в post_initial_receives/reuse_buffer). При stop буфер удалён из recv_buffers (см. handle_io:803), но в used_buffers остаётся. Деструктор used_buffers не освобождает.

Серьёзность: High — утечка ibv_mr и malloc-памяти при каждом disconnect.

Рекомендация: В деструкторе освобождать буферы из used_buffers, не из recv_buffers.


2.5. Целочисленное переполнение в send_out_size = max_msg * rdma_max_send (int)

Файл: src/client/msgr_rdma.h:79, src/client/msgr_rdma.cpp:599-601

// header:
int send_out_size = 0;

// cpp:
rc->send_out_size = rc->max_msg*rdma_max_send;

Описание: max_msg — uint64_t (до 128 МБ), rdma_max_send — uint64_t. Произведение хранится в int. При max_msg = 128 МБ, rdma_max_send = 17 произведение ≈ 2.18 ГБ превышает INT_MAX → UB, отрицательное значение.

Затем malloc_or_die((size_t)send_out_size) после неявной конвертации int → size_t для отрицательного значения превращается в огромное беззнаковое число → или malloc_or_die падает с abort, или (на некоторых системах) выделяется огромный буфер с регистрацией ibv_reg_mr.

Серьёзность: High — конфигурируется администратором, реалистично при попытке настройки большой производительности.

Рекомендация: Изменить тип send_out_size, send_out_pos, send_done_pos на uint64_t или size_t. Проверять __builtin_mul_overflow.


2.6. Отсутствие верхних границ в handle_op_hdr/handle_reply_hdr — DoS и потенциально heap-corruption

Файлы: src/client/msgr_receive.cpp:264-346 (handle_op_hdr), :401-413 (handle_reply_hdr)

// handle_op_hdr
cur_op->buf = memalign_or_die(MEM_ALIGNMENT, cur_op->req.sec_rw.len);  // len: uint64 от пира
cur_op->buf = malloc_or_die(cur_op->req.show_conf.json_len+1);          // json_len+1 — overflow при UINT64_MAX
// handle_reply_hdr
cl->read_remaining = sizeof(obj_ver_id) * op->reply.hdr.retval;         // retval: int64 от пира

Описание: Эти пути выполняются и для TCP, и для RDMA. От недоверенного или сломанного пира приходят произвольные значения len / retval. _or_die функции при len > доступной памяти вызывают abort() — DoS. Но особенно опасно: cur_op->req.show_conf.json_len + 1 при json_len = UINT64_MAX → 0 → malloc(0) (может вернуть валидный указатель!) → запись нуля в [UINT64_MAX] за пределами буфера.

Для SEC_LIST: sizeof(obj_ver_id) * retval при retval ≈ 2^60 переполняет size_t (на 64-битке sizeof(obj_ver_id) = 24, 24 * 2^60 = overflow) → выделение маленького буфера, запись больших данных → heap corruption.

Серьёзность: High (DoS) — Critical (heap-corruption через SEC_LIST overflow при злонамеренном пире).

Рекомендация: Установить разумные верхние лимиты (например, < 1 ГБ на запрос/конфиг). Использовать __builtin_add_overflow / __builtin_mul_overflow.


2.7. Хардкодный rnr_retry = 7 — бесконечные ретраи (зависание IO)

Файлы:

  • src/client/msgr_rdma.cpp:490 (OSD RC-QP RTS)
  • src/client/msgr_rdmacm.cpp:279, 450 (rdma_accept / rdma_connect conn_params)
  • src/nfs/nfs_proxy_rdma.cpp:364 (NFS rdma_accept)

Описание: Значение 7 в rnr_retry (для QP) и rnr_retry_count (для conn_params в RDMA-CM) по спецификации IBA означает «infinite retries». При флапе приёмника отправитель будет ретраить вечно, IO залипают, верхнеуровневые таймауты NFS могут быть на минуты.

Серьёзность: High — затрудняет отказоустойчивость и диагностику.

Рекомендация: Использовать 6 (6 попыток ≈ десятки миллисекунд) и полагаться на osd_ping_timeout.


2.8. cm_refs пометка «FIXME now unused» — счётчик не работает

Файлы: src/client/msgr_rdma.h:41, src/client/msgr_rdmacm.cpp:482, src/client/msgr_rdma.cpp:90

Описание: cm_refs инкрементируется в rdmacm_established, декрементируется в ~msgr_rdma_connection_t(). Но никогда не проверяется ни в одном месте. Это означает:

  • Если CM-контекст уничтожается при cm_refs > 0, будет use-after-free (см. 1.11).
  • Соединения в стадии rdmacm_connecting_t (rdmacm_create_qp уже создал QP в контексте) не учитываются вовсе — комментарий в коде сам это признаёт.

Серьёзность: High.

Рекомендация: Либо довести cm_refs до полноценного refcount'а, либо удалить как мёртвый код, явно документировав, что время жизни CM-контекста привязано к мессенджеру (что неверно, т.к. сейчас не привязано).


2.9. Хардкодный rdma_max_send = 8 — недостаточная глубина SEND для современных каналов

Файл: src/client/messenger.cpp:323-325

Описание: При линковой скорости 100/200 Гбит/с и max_msg = 129 КБ максимум в полёте = 8 × 129 КБ ≈ 1 МБ. При RTT ≈ 5 мкс это даёт лимит около 200 МБ/с на пир — в 100 раз меньше возможностей канала.

Серьёзность: High (производительность). Полностью лишает кластер преимуществ RDMA на современном железе.

Рекомендация: Поднять дефолт до 32-64. Аналогично rdma_max_recv до 32-64.


2.10. Большой объём pinned-памяти на пир (recv_buf ≈ 2 МБ)

Файл: src/client/msgr_rdma.cpp:666-668

rc->recv_buf.buf = (uint8_t*)malloc_or_die(rc->max_msg * rc->max_recv);
rc->recv_buf.mr = ibv_reg_mr(...);

Описание: На каждый пир выделяется max_msg × max_recv = 129 КБ × 16 ≈ 2 МБ pinned memory + такая же для send (max_msg × rdma_max_send = 129 КБ × 8 ≈ 1 МБ). При кластере с 500 OSD-пирами это ~1.5 ГБ pinned-памяти на узел. Возможен OOM/memlock-bust.

OSD вызывает MCL_FUTURE | MCL_CURRENT (см. osd.cpp:34-46), поэтому это давит на ulimit -l.

Серьёзность: High на больших кластерах. На малых — OK.

Рекомендация:

  • Использовать общий пул pre-registered MR-памяти (как в rdma_alloc.cpp для NFS).
  • Для recv: header-first протокол с маленьким изначальным буфером + переключение на большой при необходимости.
  • Документировать требования ulimit -l в зависимости от размера кластера.

2.11. RDMA-CM listener: bound_port разделяется между bind_address без согласования

Файл: src/osd/osd.cpp:408-424

for (auto & bind_address: bind_addresses) {
    auto listener = msgr.rdmacm_listen(bind_address, rdmacm_port, &rdmacm_port, log_level);
    ...
}

Описание: После первого вызова rdmacm_port обновляется реально выбранным портом и передаётся как in/out для второго bind. Если первый порт был auto-pick'ом (0), второй bind использует выбранное значение — но на другом интерфейсе порт может быть занят, и второй bind упадёт. Тогда iWARP-CM перестанет работать на одном из адресов.

Серьёзность: Medium-High.

Рекомендация: Явно использовать первое выбранное значение для всех или ошибаться при невозможности.


2.12. Двойное соответствие RDMA_CM_EVENT_REJECTED в handle_rdmacm_events — дублирование и потенциальная путаница

Файл: src/client/msgr_rdmacm.cpp:109-152

else if (ev->event == RDMA_CM_EVENT_CONNECT_ERROR ||
    ev->event == RDMA_CM_EVENT_REJECTED ||
    ev->event == RDMA_CM_EVENT_DISCONNECTED ||
    ev->event == RDMA_CM_EVENT_DEVICE_REMOVAL) { ... }
...
else if (ev->event == RDMA_CM_EVENT_UNREACHABLE || ev->event == RDMA_CM_EVENT_REJECTED) {
    // мёртвый код для REJECTED
    rdmacm_established(ev);
}

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

Серьёзность: Medium (как индикатор беспорядка) — High (как уязвимость к будущим правкам).

Рекомендация: Удалить лишний REJECTED из второй ветки, добавить комментарий, что REJECTED обрабатывается выше.


2.13. init_recv_rdma запускается ПОСЛЕ rdma_accept и rdmacm_connections[cmid] = cl

Файлы: src/client/msgr_rdmacm.cpp:266-298, 507-516

Описание: Сначала rdma_accept(ev->id, &conn_params), потом установка обработчика, потом, в обработчике RDMA_CM_EVENT_ESTABLISHED — init_recv_rdma(cl). Между событиями accept (сервер OK'ает соединение) и ESTABLISHED (соединение фактически установлено) клиент может уже начать слать данные. На стороне сервера ни одного RECV WR не запостено → RNR-NAK. RNR-retry компенсирует, но это лишняя задержка и нагрузка на retry-счётчики.

Серьёзность: Medium (производительность).

Рекомендация: Запостить хотя бы один RECV WR до rdma_accept.


3. ПРОБЛЕМЫ СРЕДНЕГО ПРИОРИТЕТА (Medium)

3.1. Endianness magic для NFS-RDMA-CM private_data ломает big-endian-хосты

Файл: src/nfs/nfs_proxy_rdma.cpp:23-32, 342-348, 385-396

#define NFS_RDMACM_PRIVATE_DATA_MAGIC_LE 0x180eabf6
struct __attribute__((__packed__)) nfs_rdmacm_private {
    uint32_t format_identifier; // magic, should be 0xf6ab0e18 in big endian
    ...
};

На LE: байты на проводе f6 ab 0e 18 → uint32_t = 0x180eabf6, совпадает. На BE: f6 ab 0e 18 → uint32_t = 0xf6ab0e18, не совпадает. На BE-хосте магия не работает, дефолтные параметры не согласуются (в т.ч. remote_invalidate=true, который может быть некорректным).

Рекомендация: Использовать be32toh() / htobe32() явно, имя без _LE.

3.2. NFS NFS-RDMA: remote_invalidate по умолчанию true — несовместимость с не-Linux клиентами

Файл: src/nfs/nfs_proxy_rdma.cpp:137-138

При недополучении private_data (или старом клиенте), дефолтный remote_invalidate = true приводит к отправке IBV_WR_SEND_WITH_INV. Если клиент не поддерживает remote invalidation, его rkey'и не освобождаются автоматически → утечка регистраций / зависание.

Рекомендация: Дефолт false, включать только при явной успешной негоциации.

3.3. max_send_sge = 1 в NFS заставляет копировать reply через memcpy

Файл: src/nfs/nfs_proxy_rdma.cpp:327-329, 670-680

QP создаётся с max_send_sge = 1. Все исходящие данные собираются в один буфер rop->buffer через memcpy. Это лишний цикл копирования для каждого READ-reply размером до 256 КБ. Влияние на throughput значительно.

Рекомендация: Использовать max_send_sge ≈ 16-32, собирать SEND из iovec'ов напрямую (как в rdma_alloc для готовых iovec — без копирования). Это уменьшит memory bandwidth в 2 раза для крупных READ.

3.4. num_sge = 1 для OSD RDMA-SEND — лишний memcpy в send_out

Файл: src/client/msgr_rdma.cpp:629-634

В OSD-протоколе каждое RDMA-SEND имеет один SGE. Данные из cl->send_list (iovec) сначала копируются в линейный send_out-буфер, потом отправляются. Это удваивает memory bandwidth на каждое сообщение.

Рекомендация: Использовать pre-registered memory pool (либо общий, либо per-context), отправлять SEND напрямую из iovec'ов в виде SGE list.

3.5. IBV_WR_SEND_WITH_INV без проверки device_cap — упадёт на HCA без MR_EXTENSIONS

Файл: src/nfs/nfs_proxy_rdma.cpp:741-744

Используется IBV_WR_SEND_WITH_INV при remote_invalidate=true, но никакой проверки dev_attr.device_cap_flags & IBV_DEVICE_MEM_MGT_EXTENSIONS нет. На устройствах без этой возможности ibv_post_send упадёт → exit(1) (см. 1.1).

Рекомендация: Проверять capability, при отсутствии fallback на обычный SEND и отключение remote_invalidate.

3.6. ibv_resize_cq — нет верхнего лимита и потенциально небезопасный fallback

Файл: src/client/msgr_rdma.cpp:383-403

Алгоритм удваивает max_cqe без верхнего лимита (dev_attr.max_cqe). На некоторых драйверах ibv_resize_cq может вернуть успех, фактически не изменив размер — потенциальное переполнение CQ (fatal).

Рекомендация: Кэппить по attrx.orig_attr.max_cqe. После resize вызывать ibv_query_cq для верификации.

3.7. max_send_wr / max_recv_wr не проверяются против dev_attr.max_qp_wr

Файлы: msgr_rdma.cpp:425-428, msgr_rdmacm.cpp:249-252, nfs_proxy_rdma.cpp:325-328

Если admin задаст экстремальные значения, ibv_create_qp / rdma_create_qp упадёт с ENOMEM/EINVAL — но без диагностической подсказки.

Рекомендация: Clamp по dev_attr.max_qp_wr, логировать предупреждение.

3.8. IBV_SEND_INLINE не используется — лишняя латентность на small messages

Файлы: msgr_rdma.cpp:552-569, nfs_proxy_rdma.cpp (все post_send)

max_inline_data не устанавливается (остаётся 0). Для маленьких сообщений (заголовки OSD ≈ 128 байт, ping-операции) inline-SEND экономит DMA-операцию (~0.5-1 мкс).

Рекомендация: Установить max_inline_data = 256 или 512, использовать IBV_SEND_INLINE flag для маленьких payload.

3.9. iWARP несовместимости

Файлы: msgr_rdma.cpp:601 (access=0 при reg_mr), nfs_proxy_rdma.cpp:692-707 (RDMA_WRITE длины 0)

  • На iWARP ibv_reg_mr(... access=0) для send-буфера может вернуть EINVAL — iWARP требует хотя бы IBV_ACCESS_LOCAL_WRITE.
  • iWARP запрещает IBV_WR_RDMA_WRITE нулевой длины (см. RFC 5041 §5.3). Если reply chunk обрезается до 0 — сбой.

Рекомендация: Добавить IBV_ACCESS_LOCAL_WRITE для send_out MR. При length == 0 пропускать RDMA_WRITE WR.

3.10. Хардкодные responder_resources=1, initiator_depth=1 в RDMA-CM accept/connect

Файлы: src/client/msgr_rdmacm.cpp:275-280, 446-451

Жёсткий хардкод без проверки dev_attr.max_qp_rd_atom/max_qp_init_rd_atom. На SoftRoCE/симуляторах, где max_qp_rd_atom = 0, rdma_accept/rdma_connect упадёт с EINVAL. Также блокирует производительность RDMA_READ для NFS на ConnectX-4 (16) и новых HCA.

3.11. Хардкодный backlog rdma_listen(..., 128)

Файлы: src/client/msgr_rdmacm.cpp:46, src/nfs/nfs_proxy_rdma.cpp:208

На больших кластерах при массовом переподключении (например, после рестарта OSD) backlog может переполниться → отказ части соединений.

Рекомендация: Сделать настраиваемым, дефолт 1024.

3.12. Утечка xdr_free в free_rdma_rpc_op

Файл: src/nfs/nfs_proxy_rdma.cpp:852-869

При создании rpc_op_t через malloc_or_die, in_rdma_msg = rmsg копирует pointers rdma_reads, rdma_writes, rdma_reply на XDR-allocated linked-list узлы. В free_rdma_rpc_op нет xdr_free((xdrproc_t)xdr_rdma_msg, &rop->in_rdma_msg) → утечка XDR-аллокированных узлов на каждый RPC.

3.13. wr_id = 4 для chunk-write RDMA_WRITE не обрабатывается в handle_io

Файл: src/nfs/nfs_proxy_rdma.cpp:798-848

При ошибке QP unsignaled RDMA_WRITE'ы получают FLUSH-completion. Они приходят с wr_id = 4, для которого нет ветки, и логируется «BUG: unknown wr_id = 4». Спам в логах при каждом сбое.

3.14. Бесконечный цикл goto chunk_error при маленьких max_send_size от пира

Файл: src/nfs/nfs_proxy_rdma.cpp:594-655

При злонамеренном клиенте, передающем max_recv_size = 0 (т.е. 1 КБ) в private_data, заголовок ERR_CHUNK может превысить 1 КБ, и проверка msg_size+hdr_size > max_send_size && !reply_chunk снова отправит в chunk_error → бесконечная рекурсия goto, OOM/CPU 100%.

Рекомендация: В chunk_error пропускать дополнительные проверки. Гарантировать минимальный header-fit. Не повторять chunk_error рекурсивно.

3.15. ADDR_ERROR/ROUTE_ERROR — диагностика статуса неверна при положительном ev->status

Файл: src/client/msgr_rdmacm.cpp:407-409, 440-442

ev->status > 0 ? "unknown error" : strerror(-ev->status)

Если ядро возвращает положительное значение статуса (что бывает на некоторых драйверах), сообщение всегда «unknown error». Тратится время на диагностику.

Рекомендация: strerror(abs(ev->status)).

3.16. Кредитный flow-control в NFS: рассинхрон cur_credit с реальным числом RECV WR

Файл: src/nfs/nfs_proxy_rdma.cpp:139, 379, 456-462, 509, 804-815, 867

cur_credit декрементируется при получении SEND-completion отрицательного направления (recv), а инкрементируется при free_rdma_rpc_op. Однако фактический счётчик RECV WR в очереди QP не отслеживается; теоретически возможна ситуация, когда cur_credit > 0, а в QP нет RECV WR → RNR-NAK от клиента.

Рекомендация: Перепостить RECV сразу при handle_recv, до начала обработки.

3.17. Конфигурация rdma_max_send/recv = 0 обрабатывается дефолтами, но rdma_max_msg < OSD_PACKET_SIZE не проверяется

Файл: src/client/messenger.cpp:320-331

Минимальные/максимальные границы не валидируются. rdma_max_send = 1 даст депрессивную глубину; rdma_max_msg < 128 байт приведёт к assertion-фейлам в путях обработки заголовков.

3.18. NFS-RDMA: doesn't support multi-segment write chunks

Файл: src/nfs/nfs_proxy_rdma.cpp:962-976

Только 1 chunk с 1 segment в write chunk. Клиенты (не-Linux), пересылающие multi-segment, получают ERR_CHUNK.

3.19. NFS-RDMA: модифицирует входящее сообщение rop->in_rdma_msg при partial chunk write

Файл: src/nfs/nfs_proxy_rdma.cpp:641-646

rdma_writes->entry.target.target_val[0].length = chunk_iov->iov_len модифицирует структуру, которая должна быть immutable. Не баг в текущем коде, но потенциальная мина при будущих правках.

3.20. out_of_range exception в cl->send_list.at(send_pos) не обрабатывается

Файл: src/client/msgr_rdma.cpp:756-766

Использование .at при рассинхроне приводит к необработанному exception → std::terminate.

Рекомендация: Или использовать [] (UB при OOB, но не throw) + assert, или ловить exception и stop_client.


4. ПРОБЛЕМЫ НИЗКОГО ПРИОРИТЕТА (Low) и стилистические

Файл: src/client/msgr_rdma.cpp:481

Логика «GID не пустой → is_global=1» работает для RoCE (GID всегда не пустой), но для нативной IB с локальным LID-роутингом теоретически неверна. Лучше: is_global = (link_layer == IBV_LINK_LAYER_ETHERNET || subnet_prefix != 0).

4.2. hop_limit = 64 хардкод + комментарий «FIXME»

Файл: src/client/msgr_rdma.cpp:476

Не настраивается. Для multi-DC RDMA может не хватить. Дефолт OK.

4.3. sl = 0 (service level) — отсутствует QoS

Файл: src/client/msgr_rdma.cpp:479

Без поддержки конфигурации SL нельзя интегрировать с PFC/DCQCN/lossless-классами трафика на современных RoCE-фабриках.

4.4. pkey_index = 0 — фиксированно

Файл: src/client/msgr_rdma.cpp:448

Для мультипартиционных IB-кластеров может не подойти.

4.5. timeout = 14 (~134 мс) — избыточно для локального DC

Слишком долгое ожидание detection unreachable peer (~32 секунды до полного отказа QP).

4.6. Парсинг адреса в msgr_rdma_address_t::from_string: %x без ширины

Файл: src/client/msgr_rdma.cpp:48-49

sscanf без явной ширины для qpn, psn — недостаточная валидация. Не security-проблема (значения позже проверяются в ibv_modify_qp), но стилистически плохо.

4.7. GID-сериализация без разделителя между половинами

Файл: src/client/msgr_rdma.cpp:39-42

Читабельность.

4.8. rdma_max_msg = 129*1024 — недокументированное магическое число

Файл: src/client/messenger.cpp:331

Без комментария «128 КБ payload + 1 КБ overhead».

4.9. RDMA_MSGP (deprecated в RFC 8166) отвергается без явной диагностики

4.10. chunk_iov->iov_len == 0 для READ переписывает write chunk length=0 → проблема на iWARP

4.11. ibv_get_cq_event неэффективен: read() syscall на каждое событие

Поток-event-channel + epoll → 1 syscall per notify. Альтернатива: hybrid polling (busy-spin + fallback на channel).

4.12. fcntl без проверки результата

Файлы: многие места fcntl(fd, F_SETFL, ... | O_NONBLOCK)

Возвращаемое значение не проверяется. В крайне редких случаях fcntl может вернуть -1.

4.13. RDMA-CM listener backlog не настраивается

4.14. NFS next_recv_buf — мёртвая переменная (всегда 0)

Файл: src/nfs/nfs_proxy_rdma.cpp:144, 802-803

Никогда не инкрементируется, всегда указывает на нулевой элемент recv_buffers. Читателю мисслидинг.

4.15. Отсутствие документации требований к сети

В коде нет документации, что RDMA-сеть должна быть доверенной (нет аутентификации/шифрования RDMA).

4.16. WITH_RDMACM без WITH_RDMA — нерабочая компиляция

Файлы: messenger.h:47, 170, 257, 261, 309, msgr_rdma.h:5-8

WITH_RDMACM требует WITH_RDMA (типы из msgr_rdma.h), но #error-защиты нет.

4.17. send_out_size = 0 в конце деструктора ~msgr_rdma_connection_t()

Файл: src/client/msgr_rdma.cpp:99

Мёртвый код — присвоение полю в уже удаляемом объекте.

4.18. accept_connections дублирует строку cl->peer_addr = addr;

Файл: src/client/messenger.cpp:785-786 (это TCP, но рядом с RDMA логикой)

4.19. init_recv_rdma объявлен как bool, но всегда возвращает true (или умирает)

Файл: src/client/msgr_rdma.cpp:662-680

Возвращаемое значение бесполезно. См. 1.1.

4.20. nfs_rdma_buf_t::len, mr поля без копи-семантики/move-семантики

Сейчас не вызывает проблем, но при копировании nfs_rdma_buf_t MR будет двойно зарегистрирован.


5. РЕКОМЕНДАЦИИ В ПРИОРИТЕТНОМ ПОРЯДКЕ

Приоритет 1 — немедленно (Critical)

  1. Уйти от exit(1) в RDMA-путях — заменить на stop_client/stop_session. Без этого OSD/NFS-прокси нельзя считать production-ready. (§1.1)
  2. Исправить порядок destruction в ~nfs_rdma_conn_t() — сначала QP, потом CQ. (§1.5)
  3. Исправить use-after-free в handle_io после client->stop() — отложенное удаление. (§1.2)
  4. Очистка очередей перед ~nfs_rdma_conn_t() / rdma_malloc_destroy — убрать assert/abort(). (§1.3, §1.4)
  5. Лимиты на длины полей из недоверенных XDR: target_len, read_chunk_size, wr_count. (§1.7, §1.8)
  6. Поменять порядок dereg_mr ↔ free в ~msgr_rdma_buf_t(). (§1.10)
  7. Use-after-free в handle_rdmacm_events: проверять валидность cmid перед использованием. (§1.6)
  8. Сидировать lrand48() для PSN или использовать getrandom(). (§1.9)
  9. **Race в rdmacm_set_conn_timeout callback**: использовать find, не at`. (§1.13)
  10. Утечка rdmacm_connecting_t при разрушении мессенджера. (§1.12)
  11. Доработка cm_refs или его удаление (либо защита на уровне порядка деструкции). (§1.11, §2.8)

Приоритет 2 — следующая итерация (High)

  1. Реализовать обработку ibv_get_async_event. (§2.1)
  2. Поднять min_rnr_timer до 12-16. (§2.2)
  3. Реализовать RDMA NOMSG для совместимости с RFC 8166. (§2.3)
  4. Утечка used_buffers в NFS. (§2.4)
  5. Поправить тип send_out_size и проверки __builtin_mul_overflow. (§2.5)
  6. Лимиты в handle_op_hdr/handle_reply_hdr. (§2.6)
  7. rnr_retry = 6 вместо бесконечного 7. (§2.7)
  8. rdma_max_send default = 32-64, документировать формулу. (§2.9)
  9. recv_buf через общий MR-pool, header-first. (§2.10)
  10. Чистка двойного REJECTED в handle_rdmacm_events. (§2.12)
  11. Запостить RECV до rdma_accept. (§2.13)

Приоритет 3 — рефакторинг (Medium)

  1. Endianness для NFS magic, remote_invalidate=false по умолчанию. (§3.1, §3.2)
  2. max_send_sge > 1 + scatter-gather из iovec без memcpy. (§3.3, §3.4)
  3. Capability-check для IBV_WR_SEND_WITH_INV. (§3.5)
  4. ibv_resize_cq с верхним лимитом. (§3.6)
  5. Clamp max_send_wr/max_recv_wr по device caps. (§3.7)
  6. IBV_SEND_INLINE для маленьких сообщений. (§3.8)
  7. iWARP-совместимость: LOCAL_WRITE для send MR, skip zero-byte RDMA_WRITE. (§3.9)
  8. Хардкоды responder_resources/initiator_depth = 1. (§3.10)
  9. Настраиваемый rdma_listen backlog. (§3.11)
  10. xdr_free в free_rdma_rpc_op. (§3.12)
  11. Обработать wr_id = 4. (§3.13)
  12. Защита от бесконечной рекурсии chunk_error. (§3.14)
  13. Валидация конфигурационных лимитов. (§3.17)

Приоритет 4 — улучшения (Low)

36-50. Стилистика, мёртвый код, документация (см. §4).


6. ВЫВОД

Реализация RDMA в Vitastor содержит существенные дефекты надёжности и безопасности, которые делают её непригодной для production-эксплуатации в нынешнем виде на узлах, обслуживающих недоверенный или большой объём трафика. В то же время архитектура (отдельный RDMA-мессенджер с фолбэком на TCP, поддержка RDMA-CM, отдельный путь для NFS-RDMA) выглядит разумной.

Главные системные проблемы:

  • Систематическое exit(1) на временных RDMA-сбоях — самый яркий дефект, влияющий на все ситуации.
  • Несколько потенциальных RCE/heap-corruption в NFS-RDMA через невалидированные размеры от клиента.
  • Незавершённая работа cm_refs и связанные потенциальные use-after-free на ibv_context.
  • Отсутствие наблюдения за асинхронными событиями устройства — недостаточно reactive к проблемам сети.
  • Производительность ниже возможностей оборудования из-за хардкодных rdma_max_send=8, max_send_sge=1 (NFS), отсутствия inline-SEND, лишних memcpy-копий.

После закрытия Critical-проблем (§1) можно говорить о приемлемой работоспособности; после High (§2) — о приемлемой производительности и наблюдаемости. Medium/Low — задел для дальнейшего созревания.

# Аудит реализации RDMA/RDMACM в Vitastor **Дата проведения:** 2026-05-16 **Версия кода:** ветка `master`, последний коммит `e136a1df` **Область аудита:** - `src/client/msgr_rdma.{h,cpp}` — основной RDMA-мессенджер (Verbs) - `src/client/msgr_rdmacm.cpp` — RDMA-CM (Connection Manager) реализация - `src/client/messenger.{h,cpp}` — общий мессенджер, инициализация - `src/client/msgr_receive.cpp`, `msgr_send.cpp`, `msgr_stop.cpp` — путь данных - `src/nfs/nfs_proxy_rdma.cpp` — NFS-RDMA сервер (RFC 5666 / 8166) - `src/nfs/rdma_alloc.{h,cpp}` — простой RDMA-aware аллокатор - `src/osd/osd.cpp`, `src/osd/osd_secondary.cpp`, `src/osd/osd_cluster.cpp` — интеграция в OSD **Методология:** Файлы прочитаны целиком (не grep-cherry-pick). Анализ выполнен пятью параллельными агентами по областям: жизненный цикл и cleanup, обработка ошибок, корректность протокола, безопасность/границы, конкурентность/конфигурация. Все находки независимо проверены чтением исходников. **Итог:** Найдено **>50 проблем**. Реализация RDMA в Vitastor функционально работает в «хорошую погоду» (RoCEv2, x86_64, надёжная сеть, доверенные пиры), но имеет **множественные критические дефекты**, ломающие надёжность, безопасность и производительность в реальной эксплуатации. Особое внимание разработчика заслуживают: систематическое использование `exit(1)` на любых временных ошибках RDMA; use-after-free в NFS RDMA-сервере; целочисленные переполнения с потенциально эксплуатируемыми последствиями в NFS-RPC-RDMA; систематически некорректные порядки уничтожения объектов RDMA. --- ## 1. КРИТИЧЕСКИЕ ПРОБЛЕМЫ (Critical) — требуют немедленного исправления ### 1.1. Системное использование `exit(1)` при любой ошибке RDMA — один битый клиент роняет весь процесс **Файлы и места:** - `src/client/msgr_rdma.cpp:566` — `try_send_rdma_wr` после `ibv_post_send` - `src/client/msgr_rdma.cpp:603` — `try_send_rdma` при `ibv_reg_mr` - `src/client/msgr_rdma.cpp:657` — `try_recv_rdma_wr` после `ibv_post_recv` - `src/client/msgr_rdma.cpp:670` — `init_recv_rdma` при `ibv_reg_mr` - `src/client/msgr_rdma.cpp:697` — `handle_rdma_events` при `ibv_req_notify_cq` - `src/client/msgr_rdmacm.cpp:84`, `:99` — `handle_rdmacm_events` при ошибках `rdma_get_cm_event`/`rdma_ack_cm_event` - `src/nfs/nfs_proxy_rdma.cpp:474`, `:500`, `:759`, `:783`, `:1048` — все аналогичные места в NFS-RDMA - `src/nfs/rdma_alloc.cpp:118` — `rdma_malloc_alloc` при `ibv_reg_mr` **Описание:** В коде около **13 точек**, где временная или ожидаемая ошибка вызовов libibverbs/librdmacm приводит к `exit(1)` всего процесса. В частности: - `ibv_post_send` / `ibv_post_recv` возвращают ошибку, если QP перешла в состояние `IBV_QPS_ERR` (а это происходит автоматически при любой ошибке транспортного уровня — RNR retry exceeded, transport timeout, length error и т.п.). Один сломанный клиент → весь OSD падает. - `ibv_reg_mr` падает при превышении `ulimit -l` memlock (часто 64 МБ на дефолтных дистрибутивах) или при достижении `dev_attr.max_mr` HCA. Достаточно открыть много соединений, чтобы убить OSD. - `ibv_req_notify_cq` фактически возвращает ошибку, только если CQ невалидна — что возможно, например, при асинхронном `IBV_EVENT_DEVICE_FATAL`. **Серьёзность:** Critical — практически любой производственный сценарий приводит к каскадному падению OSD. **Сценарии срабатывания:** 1. Клиент с глючным железом доводит свою QP до состояния ERROR. Сервер на следующей отправке завершает работу. 2. Узлу не хватает memlock-памяти (или достигнут лимит `max_mr`) при росте числа подключений — узел падает. 3. Hot-plug удалил RDMA-устройство в виртуальной/контейнерной среде. **Рекомендация:** - Заменить **все** `exit(1)` в RDMA/RDMACM-путях на: - для проблем с конкретным клиентом — `stop_client(client_id)`; - для проблем с контекстом устройства — пометить контекст невалидным, остановить всех клиентов на нём, отключить RDMA, не убивать процесс. - Переименовать `init_recv_rdma` так, чтобы возвращаемое `bool` действительно использовалось; сейчас функция всегда возвращает `true`, но при ошибке убивает процесс. - Аналогично `rdma_malloc_alloc` должен возвращать `NULL`, а не падать. --- ### 1.2. Use-after-free в NFS RDMA `handle_io` при ошибке WC **Файл:** `src/nfs/nfs_proxy_rdma.cpp:792-815` ```cpp if (wc[i].status != IBV_WC_SUCCESS) { fprintf(stderr, "RDMA work request failed ...\n"); conn->client->stop(); // but continue to handle events to purge the queue } if (wc[i].wr_id == 1) { auto b = conn->recv_buffers[conn->next_recv_buf]; conn->recv_buffers.erase(...); if (conn->cur_credit > 0 && !conn->client->stopped) { ... } } ``` **Описание:** При получении WC со статусом ошибки вызывается `conn->client->stop()`. Внутри `nfs_client_t::stop()` (см. `nfs_proxy.cpp`) после установки `stopped=true` при `refs <= 0` происходит **немедленное** `delete this`. `delete client` вызывает `client->destroy_rdma_conn()` → `delete rdma_conn` → `~nfs_rdma_conn_t()` → **уничтожает сам `conn`, по которому мы сейчас итерируем в `handle_io()`**. Код после `stop()` продолжает обращаться к `conn->client->stopped`, `conn->recv_buffers`, `conn->outbox` — это чтение из освобождённой памяти. **Серьёзность:** Critical — гарантированный краш или повреждение памяти при любом сетевом сбое NFS-RDMA-клиента. **Рекомендация:** Использовать паттерн отложенного удаления, как сделано в OSD (`destroy_client` через `set_immediate_ops` / refs-механику). После `client->stop()` — `break` из цикла обработки WC и не трогать `conn`. --- ### 1.3. Ассерты в `~nfs_rdma_conn_t()` приводят к `abort()` при разрыве клиента в момент работы **Файл:** `src/nfs/nfs_proxy_rdma.cpp:413-417` ```cpp nfs_rdma_conn_t::~nfs_rdma_conn_t() { assert(!outbox.size()); assert(!chunk_inbox.size()); assert(!chunk_read_postponed.size()); ... } ``` **Описание:** Деструктор требует, чтобы все три очереди были пусты. Однако стандартный сценарий разрыва: 1. Клиент отправил RPC через RDMA. 2. Сервер вызвал `post_chunk_reads(rop, true)` → `chunk_inbox.push_back(rop)` (или поместил в `outbox`/`chunk_read_postponed`). 3. Сеть рвётся / клиент уходит → приходит `RDMA_CM_EVENT_DISCONNECTED`. 4. `handle_rdmacm_events` → `stop_clients.push_back(client)` → `client->stop()` → `delete rdma_conn` → **assert срабатывает** → `abort()`. **Серьёзность:** Critical — любой разрыв клиента в момент незавершённой RDMA-операции крашит NFS-прокси целиком. **Рекомендация:** - В деструкторе (или, лучше, в `nfs_client_t::stop` до `delete rdma_conn`) очищать все три очереди, корректно освобождая `rop->buffer` через `rdma_malloc_free` и сам `rop` через `free`/`xdr_free`. - Только после очистки — `delete rdma_conn`. --- ### 1.4. `rdma_malloc_destroy` вызывает `abort()` при оставшихся живых фрагментах **Файл:** `src/nfs/rdma_alloc.cpp:73-77` ```cpp if (frag_it->second.len != frag_it->second.rgn->len) { if (force) { fprintf(stderr, "BUG: Attempt to destroy RDMA allocator while buffers are not freed yet\n"); abort(); } break; } ``` **Описание:** При уничтожении `nfs_rdma_dev_state_t` зовётся `rdma_malloc_destroy`. Если в аллокаторе остались живые фрагменты (а они там почти всегда есть при аварийном разрыве — `rop->buffer`, read-chunk-буферы), вызывается `abort()`. **Серьёзность:** Critical — крах NFS-прокси при разрыве клиента в момент работы. **Связано с:** 1.3, 1.5. **Рекомендация:** Снять `abort()` — заменить на лог + утечку (лучше), либо предварительно гарантированно очистить все фрагменты в вышестоящем коде. Самый чистый вариант: владение алокатором и QP должны быть в одних руках; перед уничтожением одного — чистить второе. --- ### 1.5. Неверный порядок уничтожения в `~nfs_rdma_conn_t()`: CQ удаляется ДО QP **Файл:** `src/nfs/nfs_proxy_rdma.cpp:418-438` ```cpp nfs_rdma_conn_t::~nfs_rdma_conn_t() { ... for (auto & b: recv_buffers) { ibv_dereg_mr(b.mr); free(b.buf); } ... if (conn_dev) { ... delete conn_dev; // <-- 1. уничтожает CQ + channel + alloc conn_dev = NULL; } if (id) { ctx->rdma_connections.erase(id); if (id->qp) { rdma_destroy_qp(id); // <-- 2. уничтожает QP ПОСЛЕ CQ } rdma_destroy_id(id); } } ``` **Описание:** `delete conn_dev` вызывает `ibv_destroy_cq(cq)`. По стандарту libibverbs, `ibv_destroy_cq` возвращает `EBUSY`, пока на CQ ссылается живой QP. Здесь QP уничтожается уже после CQ — порядок неверен. **Серьёзность:** Critical — приводит к утечке CQ/PD/channel + UB в зависимости от драйвера. На Mellanox обычно «работает», но даёт «висящие» CQ. **Рекомендация:** Поменять порядок: ```cpp if (id && id->qp) rdma_destroy_qp(id); // сначала QP if (conn_dev) { delete conn_dev; ... } // потом CQ/PD if (id) rdma_destroy_id(id); // потом ID ``` Параллельно см. 1.3, 1.4. --- ### 1.6. Use-after-free в `handle_rdmacm_events` при множественных событиях для одного `cmid` **Файлы:** - `src/client/msgr_rdmacm.cpp:73-167` - `src/nfs/nfs_proxy_rdma.cpp:245-310` **Описание:** Чтобы обойти зависание `rdma_destroy_id`, код копирует события в `events_copy` и акает их сразу. Это решает первоначальную проблему, но **создаёт новую**: если за один `poll`-цикл придёт несколько событий для одного `cmid` (например `RDMA_CM_EVENT_DISCONNECTED` + `RDMA_CM_EVENT_TIMEWAIT_EXIT`), первое событие приведёт к `stop_client(...)` → `delete rdma_conn` → `rdma_destroy_id(cmid)`. Второе событие в `events_copy` ссылается на уже уничтоженный `cmid` → use-after-free при `rdmacm_connections.find(ev->id)` (как ключе в hashmap`е`), а также при последующих `fprintf(... "...event for client %ju", cli_it->second->client_id)` и т.д. Дополнительно: события, пришедшие в канал **после** дренажа в текущем цикле, но **до** реального вызова `rdma_destroy_id`, всё ещё могут привести к зависанию `rdma_destroy_id` — обход неполон. **Серьёзность:** Critical — реальный риск падения при flapping-соединениях. **Рекомендация:** - В `events_copy` сохранять не сам `rdma_cm_event`, а минимальные нужные поля (`event`, `id`, `status`). Перед использованием `id` проверять валидность через `rdmacm_connections.find(...)` и `rdmacm_connecting.find(...)`. - Аккуратнее: перед `rdma_destroy_id` ещё раз дренировать канал на наличие событий для этого `id`. - Лучше всего: отложить `rdma_destroy_id` до начала следующего вызова `handle_rdmacm_events` через специальную «корзину для уничтожения». --- ### 1.7. Потенциальный stack buffer overflow в NFS RPC-RDMA через переполнение `wr_count` **Файл:** `src/nfs/nfs_proxy_rdma.cpp:588-655` ```cpp xdr_write_chunk *reply_chunk = rop->in_rdma_msg.rdma_body.rdma_msg.rdma_reply; int reply_chunk_wr_count = (reply_chunk ? reply_chunk->target.target_len : 0); uint32_t wr_count = 1 + (chunk_iov ? 1 : 0) + (reply_chunk ? reply_chunk_wr_count : 0); if (wr_count > ctx->max_send_wr) { ... ERR_CHUNK ... } ... ibv_sge sges[wr_count]; // VLA на стеке ibv_send_wr wrs[wr_count]; // VLA на стеке ... for (uint32_t i = 0; i < reply_chunk->target.target_len && pos < msg_size; i++) { sges[wr_pos] = {...}; wrs[wr_pos] = ...; wr_pos++; } ``` **Описание:** `reply_chunk->target.target_len` — это `u_int` (unsigned int), пришедший с провода от клиента. Если, например, `target_len = 0xFFFFFFFE`: - `reply_chunk_wr_count = 0xFFFFFFFE` (int, но интерпретируется в uint32); - `wr_count = 1 + 1 + 0xFFFFFFFE = 0` (переполнение uint32); - проверка `wr_count > ctx->max_send_wr` пройдёт (`0 < 1024`); - VLA `sges[0]`/`wrs[0]` (или 1-2) на стеке; - цикл по `target_len = 0xFFFFFFFE` итераций пишет в `sges[wr_pos++]` далеко за пределами VLA → **переполнение стека**. Аналогично — `xdr_xdr_read_list` / `xdr_xdr_write_list` декодеры (`src/nfs/proto/rpc_rdma_xdr.cpp:34-42, 54-62`) рекурсивно, без верхнего лимита, спускаются по связному списку — при длинном входном списке вызывают **stack overflow при декодировании** (классический DoS через malformed packet). **Серьёзность:** Critical — потенциально удалённо эксплуатируемое условие. CVE-уровень. **Рекомендация:** - Жёстко лимитировать `target_len` сразу после XDR-декодирования (например, ≤ 64). - Использовать `__builtin_add_overflow` для расчёта `wr_count`. - Использовать `std::vector` или фиксированный размер вместо VLA. - Переписать XDR-декодеры `xdr_xdr_read_list`/`xdr_xdr_write_list` итеративно с лимитом длины. --- ### 1.8. Целочисленное переполнение в `rdma_malloc_alloc` ведёт к heap-overflow **Файл:** `src/nfs/rdma_alloc.cpp:104-145`, в связке с `src/nfs/nfs_proxy_rdma.cpp:988-1054` ```cpp size_t alloc_size = ((size + self->rdma_alloc_size - 1) / self->rdma_alloc_size) * self->rdma_alloc_size; ``` **Описание:** В `nfs_rdma_conn_t::post_chunk_reads` `read_chunk_size` накапливается из недоверенных значений `cur->entry.target.length` (uint32_t). С учётом существующей проверки `read_chunk_count > max_rdma_reads` (≤ 255), теоретически возможна сумма около `255 * 4 ГБ ≈ 1 ТБ`. На 64-битной системе это не превышает `SIZE_MAX`, но при округлении вверх: ``` size_t alloc_size = ((size + rdma_alloc_size - 1) / rdma_alloc_size) * rdma_alloc_size; ``` если `size ≈ SIZE_MAX - 100`, то `size + rdma_alloc_size - 1` **переполняется**, давая маленькое значение → `malloc_or_die(маленький размер)` + `ibv_reg_mr` на «настоящий размер» (но MR регистрируется на тот же `r->len = alloc_size_HUGE`, что и в `malloc`-параметре). Далее `pos += cur->entry.target.length` пишет байты RDMA-Read'ом за пределами выделенного буфера → heap overflow. **Серьёзность:** Critical (если достижимо через малых-чанков-много, либо одного большого) — потенциальный RCE через heap-corruption. **Рекомендация:** - Проверять `__builtin_add_overflow(size, rdma_alloc_size - 1, &intermediate)` перед делением/умножением. - Установить верхний лимит `read_chunk_size` (например `max_send_wr * 256 КБ = 256 МБ`). - Сделать аналогичные проверки везде, где сумма пользовательских `length` идёт в malloc. --- ### 1.9. Использование `lrand48()` для PSN без `srand48()` — предсказуемые PSN **Файл:** `src/client/msgr_rdma.cpp:443` ```cpp conn->addr.psn = lrand48() & 0xffffff; ``` **Описание:** `srand48()` нигде в OSD/RDMA-коде не вызывается, поэтому `lrand48()` инициализирована константным сидом `1` (POSIX), и PSN-последовательность одинакова между всеми OSD и сохраняется между перезапусками. PSN (Packet Sequence Number) в RC-QP — единственная не-инвариантная по адресу защита от стейл/спуф-пакетов. На предсказуемых PSN атакующий с доступом к сети способен инжектить «правильные» пакеты в установленное RDMA-соединение. **Серьёзность:** Critical (модель угроз "atomic untrusted network"). Для типичного дата-центра, где RDMA-сеть считается доверенной, проблема меньше — но и в этом случае стейл-пакеты предсказуемы и могут запутывать диагностику. **Рекомендация:** Перед первым `lrand48()` инициализировать `srand48(...)` от `clock_gettime(CLOCK_MONOTONIC)+pid` или, лучше, использовать `getrandom()` напрямую. --- ### 1.10. Неверный порядок в `~msgr_rdma_buf_t()`: `free(buf)` до `ibv_dereg_mr(mr)` **Файл:** `src/client/msgr_rdma.cpp:68-80` ```cpp msgr_rdma_buf_t::~msgr_rdma_buf_t() { if (buf) { free(buf); buf = NULL; } if (mr) { ibv_dereg_mr(mr); mr = NULL; } } ``` **Описание:** По спецификации libibverbs, `ibv_dereg_mr` должно вызываться **до** освобождения подложенной памяти. После `free(buf)`, hardware ещё может иметь outstanding DMA, использующих MR, который ссылается на освобождённую память → UB / повреждение данных / crash на некоторых драйверах. На практике, к моменту попадания сюда QP уже уничтожен, и DMA должно быть завершено, но порядок остаётся UB и небезопасным паттерном. **Серьёзность:** High–Critical (зависит от драйвера). На Mellanox обычно «работает», на iWARP может крашить. **Рекомендация:** Поменять порядок: сначала `ibv_dereg_mr(mr)`, затем `free(buf)`. Это однострочное исправление. --- ### 1.11. Use-after-free на `ibv_context` в `~osd_messenger_t()` для is_cm-контекстов **Файлы:** `src/client/messenger.cpp:264-299`, в связке с `src/client/msgr_rdma.cpp:56-66`, `src/client/msgr_rdmacm.cpp:169-203` **Описание:** При уничтожении мессенджера: 1. Сначала идёт цикл `stop_client(..., force_delete=true)`. Каждый клиент с RDMA-CM-соединением через `~msgr_rdma_connection_t()` вызывает `rdma_destroy_qp(cmid)` + `rdma_destroy_id(cmid)`. `rdma_destroy_id` потенциально освобождает `ibv_context`, на который ссылается общий `msgr_rdma_context_t` (`is_cm=true`). 2. Затем идёт цикл по `rdma_contexts`, который вызывает `delete rdma_context` → `~msgr_rdma_context_t()` → `ibv_destroy_cq`, `ibv_destroy_comp_channel`, `ibv_dealloc_pd` поверх **уже потенциально мёртвого** `ibv_context`. В теории `ibv_context` рефкаунтится внутренне (CQ/PD держат ссылку), и пока их не уничтожить, контекст жив. Однако `cm_refs` в коде помечен как `FIXME now unused` (см. `msgr_rdma.h:41`, `msgr_rdmacm.cpp:482`) — поле инкрементируется и декрементируется, но **никогда не проверяется**. То есть в `~osd_messenger_t()` нет проверки, что для CM-контекста все QP действительно уничтожены. **Серьёзность:** Critical (если рассинхрон порядка деструкции произойдёт). **Рекомендация:** - Довести до конца `cm_refs`: уничтожать `is_cm` `msgr_rdma_context_t` только когда `cm_refs == 0` И ни один `rdmacm_connecting_t` его не держит. - Учитывать в `cm_refs` также `rdmacm_connecting_t` (`+1` в `rdmacm_address_resolved`, `-1` в `rdmacm_on_connect_peer_error`/`rdmacm_established`). --- ### 1.12. Утечка `rdmacm_connecting_t` при разрушении мессенджера **Файл:** `src/client/messenger.cpp:264-299` (деструктор) **Описание:** `~osd_messenger_t()` останавливает клиентов (`stop_client`), уничтожает контексты, уничтожает `rdmacm_evch`, но **никогда** не итерирует по `rdmacm_connecting`. Незавершённые соединения остаются — это `cmid` + активные таймеры. Когда таймер сработает позже, лямбда захватывает `this` (мёртвый `osd_messenger_t`) → segfault. **Серьёзность:** Critical (при тестах/рестартах). **Рекомендация:** Перед `rdma_destroy_event_channel(rdmacm_evch)` пройтись по `rdmacm_connecting`, очистить таймеры, уничтожить `cmid`, удалить структуры. --- ### 1.13. Race в `rdmacm_set_conn_timeout`: callback может ссылаться на удалённый `cmid` **Файл:** `src/client/msgr_rdmacm.cpp:301-317` ```cpp conn->timeout_id = tfd->set_timer(1000*peer_connect_timeout, false, [this, cmid = conn->cmid](int timer_id) { auto conn = rdmacm_connecting.at(cmid); // <-- может бросить out_of_range ... }); ``` **Описание:** Лямбда захватывает raw-pointer `cmid`. Если до срабатывания таймера соединение перешло в установленное состояние или было удалено через `rdmacm_on_connect_peer_error`, `rdmacm_connecting.at(cmid)` бросит `out_of_range` — необработанное исключение → `std::terminate` → `abort`. `tfd->clear_timer(conn->timeout_id)` пытается отменить таймер, но если callback уже встал в очередь обработки и `clear_timer` срабатывает в окне между запуском и фактическим execution — поведение зависит от реализации `timerfd_manager_t`. **Серьёзность:** Critical (зависит от семантики `clear_timer`). **Рекомендация:** - Внутри callback'а использовать `find`, не `at`. Если не нашли — `return`. - Лучше: вместо raw-pointer `cmid` использовать generation-counter или захватывать `weak_ptr` на `conn`. --- ## 2. ВЫСОКОПРИОРИТЕТНЫЕ ПРОБЛЕМЫ (High) ### 2.1. Отсутствие обработки асинхронных событий устройств — `ibv_get_async_event` не используется НИГДЕ **Файлы:** весь RDMA-код **Описание:** В Vitastor нет обращений к `ibv_get_async_event` / `ibv_ack_async_event`. Это значит, что игнорируются критические события устройства: - `IBV_EVENT_DEVICE_FATAL` — фатальная ошибка HCA - `IBV_EVENT_PORT_ERR` / `IBV_EVENT_PORT_ACTIVE` — состояние порта (вышел/восстановился) - `IBV_EVENT_LID_CHANGE`, `IBV_EVENT_GID_CHANGE` — переконфигурация subnet - `IBV_EVENT_CLIENT_REREGISTER` — переподпись MR требуется - `IBV_EVENT_QP_FATAL`, `IBV_EVENT_QP_REQ_ERR`, `IBV_EVENT_QP_ACCESS_ERR` — фатальные ошибки QP - `IBV_EVENT_CQ_ERR`, `IBV_EVENT_SRQ_ERR` — фатальные ошибки CQ/SRQ **Сценарии:** - Порт IB ушёл в ERR (отсоединён кабель). Кластер заметит проблему только через TCP-keepalive (через десятки секунд) или истечение retry_cnt в RC-QP (≈ минуты). - Hot-plug RDMA-устройства в виртуальной среде → процесс висит, считая соединения живыми. - LID-change на IB-фабрике → новые соединения не устанавливаются, диагностики нет. **Серьёзность:** High — критично для production. Долгоживущие OSD не способны корректно реагировать на изменения состояния сети/устройства. **Рекомендация:** Открыть `ibv_context->async_fd` (он уже non-blocking), повесить epoll-handler, в нём вызывать `ibv_get_async_event` + `ibv_ack_async_event`, и обрабатывать события: - Fatal device/port — остановить все клиенты на этом контексте, попытаться переинициализировать. - LID/GID change — переподключить соединения. - QP/CQ errors — остановить соответствующих клиентов. --- ### 2.2. `min_rnr_timer = 1` (10 микросекунд) — лавины RNR-retries **Файл:** `src/client/msgr_rdma.cpp:487` ```cpp .min_rnr_timer = 1, .timeout = 14, .retry_cnt = 7, .rnr_retry = 7, ``` **Описание:** По IBA spec §11.4.1.1.5, `min_rnr_timer` — это **прямой** индекс таблицы, где `1 = 0.01 мс (10 мкс)`, `12 = 0.64 мс`, `16 = 2.56 мс`, `0 = 655.36 мс`. Комментарий в коде «4.096us*2^(timeout+1)» относится к ack-`timeout`, не к `min_rnr_timer` — кодом ошибочно объединили формулы. С `min_rnr_timer = 1` (10 мкс), при возникновении RNR-NAK (Receiver Not Ready — приёмник не успел запостить RECV WR), отправитель ретраит через 10 мкс. Если приёмник под нагрузкой и не успевает за 10 мкс, лавина из `rnr_retry = 7` попыток истощится за миллисекунды, после чего QP уйдёт в ERR. Отсюда `IBV_WC_RNR_RETRY_EXC_ERR` → `stop_client` (или, до исправления 1.1 — `exit(1)`). **Серьёзность:** High — реальные сетевые/нагрузочные сценарии вызывают преждевременные разрывы. **Рекомендация:** Установить `min_rnr_timer = 12` (0.64 мс) минимально, лучше `16` (2.56 мс). Сделать настраиваемым через `rdma_min_rnr_timer`. Дополнительно: `rnr_retry = 7` — особое значение «бесконечные ретраи». Это значит, что при «висящем» приёмнике отправитель никогда не сдастся — IO залипает навсегда. Лучше использовать `6` (6 попыток) и полагаться на верхнеуровневые таймауты. --- ### 2.3. RDMA `NOMSG` не поддерживается — нарушение RFC 8166 **Файл:** `src/nfs/nfs_proxy_rdma.cpp:895-916` ```cpp if (rmsg.rdma_vers != 1 || rmsg.rdma_body.proc != RDMA_MSG) { // ERR_VERS / ERR_CHUNK } ``` **Описание:** Принимается только `RDMA_MSG`. `RDMA_NOMSG` (RFC 8166 §3.7, тип сообщения, в котором payload передаётся целиком в read/reply-чанках, а SEND содержит только заголовок) **не реализован**. Linux NFS-клиент использует `RDMA_NOMSG` для крупных запросов с inline-частью больше `inline_threshold`. **Серьёзность:** High — нарушение совместимости с RFC. Часть сценариев NFS-RDMA не работает с Vitastor. **Рекомендация:** Добавить обработку `RDMA_NOMSG`: после получения SEND с типом `NOMSG` сделать `RDMA_READ` на reply chunk, и далее декодировать XDR. --- ### 2.4. NFS RDMA: утечка `used_buffers` при остановке клиента **Файл:** `src/nfs/nfs_proxy_rdma.cpp:481-484` (`post_recv` с проверкой `stopped`), `:418-423` (деструктор по `recv_buffers`) **Описание:** В `post_recv` есть `if (client->stopped) return;`. В `~nfs_rdma_conn_t()` освобождаются буферы из `recv_buffers`. Но между ними буфер живёт в `std::map<void*, nfs_rdma_buf_t> used_buffers` (туда заносится в `post_initial_receives`/`reuse_buffer`). При `stop` буфер удалён из `recv_buffers` (см. `handle_io:803`), но в `used_buffers` остаётся. Деструктор `used_buffers` не освобождает. **Серьёзность:** High — утечка `ibv_mr` и `malloc`-памяти при каждом disconnect. **Рекомендация:** В деструкторе освобождать буферы из `used_buffers`, не из `recv_buffers`. --- ### 2.5. Целочисленное переполнение в `send_out_size = max_msg * rdma_max_send` (`int`) **Файл:** `src/client/msgr_rdma.h:79`, `src/client/msgr_rdma.cpp:599-601` ```cpp // header: int send_out_size = 0; // cpp: rc->send_out_size = rc->max_msg*rdma_max_send; ``` **Описание:** `max_msg` — `uint64_t` (до 128 МБ), `rdma_max_send` — `uint64_t`. Произведение хранится в `int`. При `max_msg = 128 МБ, rdma_max_send = 17` произведение `≈ 2.18 ГБ` превышает `INT_MAX` → UB, отрицательное значение. Затем `malloc_or_die((size_t)send_out_size)` после неявной конвертации `int → size_t` для отрицательного значения превращается в **огромное беззнаковое** число → или `malloc_or_die` падает с `abort`, или (на некоторых системах) выделяется огромный буфер с регистрацией `ibv_reg_mr`. **Серьёзность:** High — конфигурируется администратором, реалистично при попытке настройки большой производительности. **Рекомендация:** Изменить тип `send_out_size`, `send_out_pos`, `send_done_pos` на `uint64_t` или `size_t`. Проверять `__builtin_mul_overflow`. --- ### 2.6. Отсутствие верхних границ в `handle_op_hdr`/`handle_reply_hdr` — DoS и потенциально heap-corruption **Файлы:** `src/client/msgr_receive.cpp:264-346` (`handle_op_hdr`), `:401-413` (`handle_reply_hdr`) ```cpp // handle_op_hdr cur_op->buf = memalign_or_die(MEM_ALIGNMENT, cur_op->req.sec_rw.len); // len: uint64 от пира cur_op->buf = malloc_or_die(cur_op->req.show_conf.json_len+1); // json_len+1 — overflow при UINT64_MAX // handle_reply_hdr cl->read_remaining = sizeof(obj_ver_id) * op->reply.hdr.retval; // retval: int64 от пира ``` **Описание:** Эти пути выполняются и для TCP, и для RDMA. От недоверенного или сломанного пира приходят произвольные значения `len` / `retval`. `_or_die` функции при `len > доступной памяти` вызывают `abort()` — DoS. Но особенно опасно: `cur_op->req.show_conf.json_len + 1` при `json_len = UINT64_MAX` → `0` → `malloc(0)` (может вернуть валидный указатель!) → запись нуля в `[UINT64_MAX]` за пределами буфера. Для `SEC_LIST`: `sizeof(obj_ver_id) * retval` при `retval ≈ 2^60` переполняет `size_t` (на 64-битке `sizeof(obj_ver_id) = 24`, `24 * 2^60 = overflow`) → выделение маленького буфера, запись больших данных → heap corruption. **Серьёзность:** High (DoS) — Critical (heap-corruption через SEC_LIST overflow при злонамеренном пире). **Рекомендация:** Установить разумные верхние лимиты (например, `< 1 ГБ` на запрос/конфиг). Использовать `__builtin_add_overflow` / `__builtin_mul_overflow`. --- ### 2.7. Хардкодный `rnr_retry = 7` — бесконечные ретраи (зависание IO) **Файлы:** - `src/client/msgr_rdma.cpp:490` (OSD RC-QP RTS) - `src/client/msgr_rdmacm.cpp:279, 450` (rdma_accept / rdma_connect conn_params) - `src/nfs/nfs_proxy_rdma.cpp:364` (NFS rdma_accept) **Описание:** Значение `7` в `rnr_retry` (для QP) и `rnr_retry_count` (для conn_params в RDMA-CM) по спецификации IBA означает «infinite retries». При флапе приёмника отправитель будет ретраить вечно, IO залипают, верхнеуровневые таймауты NFS могут быть на минуты. **Серьёзность:** High — затрудняет отказоустойчивость и диагностику. **Рекомендация:** Использовать `6` (6 попыток ≈ десятки миллисекунд) и полагаться на `osd_ping_timeout`. --- ### 2.8. `cm_refs` пометка «FIXME now unused» — счётчик не работает **Файлы:** `src/client/msgr_rdma.h:41`, `src/client/msgr_rdmacm.cpp:482`, `src/client/msgr_rdma.cpp:90` **Описание:** `cm_refs` инкрементируется в `rdmacm_established`, декрементируется в `~msgr_rdma_connection_t()`. Но **никогда не проверяется** ни в одном месте. Это означает: - Если CM-контекст уничтожается при `cm_refs > 0`, будет use-after-free (см. 1.11). - Соединения в стадии `rdmacm_connecting_t` (`rdmacm_create_qp` уже создал QP в контексте) не учитываются вовсе — комментарий в коде сам это признаёт. **Серьёзность:** High. **Рекомендация:** Либо довести `cm_refs` до полноценного refcount'а, либо удалить как мёртвый код, явно документировав, что время жизни CM-контекста привязано к мессенджеру (что неверно, т.к. сейчас не привязано). --- ### 2.9. Хардкодный `rdma_max_send = 8` — недостаточная глубина SEND для современных каналов **Файл:** `src/client/messenger.cpp:323-325` **Описание:** При линковой скорости 100/200 Гбит/с и `max_msg = 129 КБ` максимум в полёте `= 8 × 129 КБ ≈ 1 МБ`. При RTT ≈ 5 мкс это даёт лимит около `200 МБ/с` на пир — **в 100 раз меньше** возможностей канала. **Серьёзность:** High (производительность). Полностью лишает кластер преимуществ RDMA на современном железе. **Рекомендация:** Поднять дефолт до `32-64`. Аналогично `rdma_max_recv` до `32-64`. --- ### 2.10. Большой объём pinned-памяти на пир (recv_buf ≈ 2 МБ) **Файл:** `src/client/msgr_rdma.cpp:666-668` ```cpp rc->recv_buf.buf = (uint8_t*)malloc_or_die(rc->max_msg * rc->max_recv); rc->recv_buf.mr = ibv_reg_mr(...); ``` **Описание:** На каждый пир выделяется `max_msg × max_recv = 129 КБ × 16 ≈ 2 МБ` pinned memory + такая же для send (`max_msg × rdma_max_send = 129 КБ × 8 ≈ 1 МБ`). При кластере с 500 OSD-пирами это `~1.5 ГБ` pinned-памяти на узел. Возможен OOM/memlock-bust. OSD вызывает `MCL_FUTURE | MCL_CURRENT` (см. `osd.cpp:34-46`), поэтому это давит на `ulimit -l`. **Серьёзность:** High на больших кластерах. На малых — OK. **Рекомендация:** - Использовать общий пул pre-registered MR-памяти (как в `rdma_alloc.cpp` для NFS). - Для recv: header-first протокол с маленьким изначальным буфером + переключение на большой при необходимости. - Документировать требования `ulimit -l` в зависимости от размера кластера. --- ### 2.11. RDMA-CM listener: bound_port разделяется между bind_address без согласования **Файл:** `src/osd/osd.cpp:408-424` ```cpp for (auto & bind_address: bind_addresses) { auto listener = msgr.rdmacm_listen(bind_address, rdmacm_port, &rdmacm_port, log_level); ... } ``` **Описание:** После первого вызова `rdmacm_port` обновляется реально выбранным портом и передаётся как in/out для второго bind. Если первый порт был auto-pick'ом (0), второй bind использует выбранное значение — но на другом интерфейсе порт может быть занят, и второй bind упадёт. Тогда iWARP-CM перестанет работать на одном из адресов. **Серьёзность:** Medium-High. **Рекомендация:** Явно использовать первое выбранное значение для всех или ошибаться при невозможности. --- ### 2.12. Двойное соответствие `RDMA_CM_EVENT_REJECTED` в `handle_rdmacm_events` — дублирование и потенциальная путаница **Файл:** `src/client/msgr_rdmacm.cpp:109-152` ```cpp else if (ev->event == RDMA_CM_EVENT_CONNECT_ERROR || ev->event == RDMA_CM_EVENT_REJECTED || ev->event == RDMA_CM_EVENT_DISCONNECTED || ev->event == RDMA_CM_EVENT_DEVICE_REMOVAL) { ... } ... else if (ev->event == RDMA_CM_EVENT_UNREACHABLE || ev->event == RDMA_CM_EVENT_REJECTED) { // мёртвый код для REJECTED rdmacm_established(ev); } ``` **Описание:** Второе сравнение `REJECTED` недостижимо. Это явный знак, что код проходил несколько правок без чистки. Хотя сегодня логика корректна, в будущем правки могут добавить специфическое поведение во вторую ветку, не зная про первую. **Серьёзность:** Medium (как индикатор беспорядка) — High (как уязвимость к будущим правкам). **Рекомендация:** Удалить лишний `REJECTED` из второй ветки, добавить комментарий, что REJECTED обрабатывается выше. --- ### 2.13. `init_recv_rdma` запускается ПОСЛЕ `rdma_accept` и `rdmacm_connections[cmid] = cl` **Файлы:** `src/client/msgr_rdmacm.cpp:266-298, 507-516` **Описание:** Сначала `rdma_accept(ev->id, &conn_params)`, потом установка обработчика, потом, **в обработчике `RDMA_CM_EVENT_ESTABLISHED`** — `init_recv_rdma(cl)`. Между событиями `accept` (сервер OK'ает соединение) и `ESTABLISHED` (соединение фактически установлено) клиент может уже начать слать данные. На стороне сервера ни одного RECV WR не запостено → RNR-NAK. RNR-retry компенсирует, но это лишняя задержка и нагрузка на retry-счётчики. **Серьёзность:** Medium (производительность). **Рекомендация:** Запостить хотя бы один RECV WR до `rdma_accept`. --- ## 3. ПРОБЛЕМЫ СРЕДНЕГО ПРИОРИТЕТА (Medium) ### 3.1. Endianness magic для NFS-RDMA-CM private_data ломает big-endian-хосты **Файл:** `src/nfs/nfs_proxy_rdma.cpp:23-32, 342-348, 385-396` ```cpp #define NFS_RDMACM_PRIVATE_DATA_MAGIC_LE 0x180eabf6 struct __attribute__((__packed__)) nfs_rdmacm_private { uint32_t format_identifier; // magic, should be 0xf6ab0e18 in big endian ... }; ``` На LE: байты на проводе `f6 ab 0e 18` → `uint32_t = 0x180eabf6`, совпадает. На BE: `f6 ab 0e 18` → `uint32_t = 0xf6ab0e18`, не совпадает. На BE-хосте магия не работает, дефолтные параметры не согласуются (в т.ч. `remote_invalidate=true`, который может быть некорректным). **Рекомендация:** Использовать `be32toh()` / `htobe32()` явно, имя без `_LE`. ### 3.2. NFS NFS-RDMA: `remote_invalidate` по умолчанию `true` — несовместимость с не-Linux клиентами **Файл:** `src/nfs/nfs_proxy_rdma.cpp:137-138` При недополучении private_data (или старом клиенте), дефолтный `remote_invalidate = true` приводит к отправке `IBV_WR_SEND_WITH_INV`. Если клиент не поддерживает remote invalidation, его rkey'и не освобождаются автоматически → утечка регистраций / зависание. **Рекомендация:** Дефолт `false`, включать только при явной успешной негоциации. ### 3.3. `max_send_sge = 1` в NFS заставляет копировать reply через `memcpy` **Файл:** `src/nfs/nfs_proxy_rdma.cpp:327-329, 670-680` QP создаётся с `max_send_sge = 1`. Все исходящие данные собираются в один буфер `rop->buffer` через `memcpy`. Это лишний цикл копирования для каждого READ-reply размером до 256 КБ. Влияние на throughput значительно. **Рекомендация:** Использовать `max_send_sge ≈ 16-32`, собирать SEND из iovec'ов напрямую (как в `rdma_alloc` для готовых iovec — без копирования). Это уменьшит memory bandwidth в 2 раза для крупных READ. ### 3.4. `num_sge = 1` для OSD RDMA-SEND — лишний `memcpy` в send_out **Файл:** `src/client/msgr_rdma.cpp:629-634` В OSD-протоколе каждое RDMA-SEND имеет один SGE. Данные из `cl->send_list` (iovec) сначала копируются в линейный `send_out`-буфер, потом отправляются. Это удваивает memory bandwidth на каждое сообщение. **Рекомендация:** Использовать pre-registered memory pool (либо общий, либо per-context), отправлять SEND напрямую из iovec'ов в виде SGE list. ### 3.5. `IBV_WR_SEND_WITH_INV` без проверки device_cap — упадёт на HCA без MR_EXTENSIONS **Файл:** `src/nfs/nfs_proxy_rdma.cpp:741-744` Используется `IBV_WR_SEND_WITH_INV` при `remote_invalidate=true`, но никакой проверки `dev_attr.device_cap_flags & IBV_DEVICE_MEM_MGT_EXTENSIONS` нет. На устройствах без этой возможности `ibv_post_send` упадёт → `exit(1)` (см. 1.1). **Рекомендация:** Проверять capability, при отсутствии fallback на обычный SEND и отключение `remote_invalidate`. ### 3.6. `ibv_resize_cq` — нет верхнего лимита и потенциально небезопасный fallback **Файл:** `src/client/msgr_rdma.cpp:383-403` Алгоритм удваивает `max_cqe` без верхнего лимита (`dev_attr.max_cqe`). На некоторых драйверах `ibv_resize_cq` может вернуть успех, фактически не изменив размер — потенциальное переполнение CQ (fatal). **Рекомендация:** Кэппить по `attrx.orig_attr.max_cqe`. После resize вызывать `ibv_query_cq` для верификации. ### 3.7. `max_send_wr` / `max_recv_wr` не проверяются против `dev_attr.max_qp_wr` **Файлы:** `msgr_rdma.cpp:425-428`, `msgr_rdmacm.cpp:249-252`, `nfs_proxy_rdma.cpp:325-328` Если admin задаст экстремальные значения, `ibv_create_qp` / `rdma_create_qp` упадёт с ENOMEM/EINVAL — но без диагностической подсказки. **Рекомендация:** Clamp по `dev_attr.max_qp_wr`, логировать предупреждение. ### 3.8. `IBV_SEND_INLINE` не используется — лишняя латентность на small messages **Файлы:** `msgr_rdma.cpp:552-569`, `nfs_proxy_rdma.cpp` (все `post_send`) `max_inline_data` не устанавливается (остаётся 0). Для маленьких сообщений (заголовки OSD ≈ 128 байт, ping-операции) inline-SEND экономит DMA-операцию (~0.5-1 мкс). **Рекомендация:** Установить `max_inline_data = 256` или `512`, использовать `IBV_SEND_INLINE` flag для маленьких payload. ### 3.9. iWARP несовместимости **Файлы:** `msgr_rdma.cpp:601` (`access=0` при reg_mr), `nfs_proxy_rdma.cpp:692-707` (RDMA_WRITE длины 0) - На iWARP `ibv_reg_mr(... access=0)` для send-буфера может вернуть EINVAL — iWARP требует хотя бы `IBV_ACCESS_LOCAL_WRITE`. - iWARP запрещает `IBV_WR_RDMA_WRITE` нулевой длины (см. RFC 5041 §5.3). Если reply chunk обрезается до 0 — сбой. **Рекомендация:** Добавить `IBV_ACCESS_LOCAL_WRITE` для send_out MR. При `length == 0` пропускать RDMA_WRITE WR. ### 3.10. Хардкодные `responder_resources=1, initiator_depth=1` в RDMA-CM accept/connect **Файлы:** `src/client/msgr_rdmacm.cpp:275-280, 446-451` Жёсткий хардкод без проверки `dev_attr.max_qp_rd_atom`/`max_qp_init_rd_atom`. На SoftRoCE/симуляторах, где `max_qp_rd_atom = 0`, `rdma_accept`/`rdma_connect` упадёт с EINVAL. Также блокирует производительность RDMA_READ для NFS на ConnectX-4 (16) и новых HCA. ### 3.11. Хардкодный backlog `rdma_listen(..., 128)` **Файлы:** `src/client/msgr_rdmacm.cpp:46`, `src/nfs/nfs_proxy_rdma.cpp:208` На больших кластерах при массовом переподключении (например, после рестарта OSD) backlog может переполниться → отказ части соединений. **Рекомендация:** Сделать настраиваемым, дефолт `1024`. ### 3.12. Утечка `xdr_free` в `free_rdma_rpc_op` **Файл:** `src/nfs/nfs_proxy_rdma.cpp:852-869` При создании `rpc_op_t` через `malloc_or_die`, `in_rdma_msg = rmsg` копирует pointers `rdma_reads`, `rdma_writes`, `rdma_reply` на XDR-allocated linked-list узлы. В `free_rdma_rpc_op` нет `xdr_free((xdrproc_t)xdr_rdma_msg, &rop->in_rdma_msg)` → утечка XDR-аллокированных узлов на каждый RPC. ### 3.13. `wr_id = 4` для chunk-write RDMA_WRITE не обрабатывается в `handle_io` **Файл:** `src/nfs/nfs_proxy_rdma.cpp:798-848` При ошибке QP unsignaled RDMA_WRITE'ы получают FLUSH-completion. Они приходят с `wr_id = 4`, для которого нет ветки, и логируется «BUG: unknown wr_id = 4». Спам в логах при каждом сбое. ### 3.14. Бесконечный цикл `goto chunk_error` при маленьких `max_send_size` от пира **Файл:** `src/nfs/nfs_proxy_rdma.cpp:594-655` При злонамеренном клиенте, передающем `max_recv_size = 0` (т.е. 1 КБ) в private_data, заголовок ERR_CHUNK может превысить 1 КБ, и проверка `msg_size+hdr_size > max_send_size && !reply_chunk` снова отправит в `chunk_error` → бесконечная рекурсия goto, OOM/CPU 100%. **Рекомендация:** В `chunk_error` пропускать дополнительные проверки. Гарантировать минимальный header-fit. Не повторять `chunk_error` рекурсивно. ### 3.15. ADDR_ERROR/ROUTE_ERROR — диагностика статуса неверна при положительном `ev->status` **Файл:** `src/client/msgr_rdmacm.cpp:407-409, 440-442` ```cpp ev->status > 0 ? "unknown error" : strerror(-ev->status) ``` Если ядро возвращает положительное значение статуса (что бывает на некоторых драйверах), сообщение всегда «unknown error». Тратится время на диагностику. **Рекомендация:** `strerror(abs(ev->status))`. ### 3.16. Кредитный flow-control в NFS: рассинхрон `cur_credit` с реальным числом RECV WR **Файл:** `src/nfs/nfs_proxy_rdma.cpp:139, 379, 456-462, 509, 804-815, 867` `cur_credit` декрементируется при получении SEND-completion отрицательного направления (`recv`), а инкрементируется при `free_rdma_rpc_op`. Однако фактический счётчик RECV WR в очереди QP не отслеживается; теоретически возможна ситуация, когда `cur_credit > 0`, а в QP нет RECV WR → RNR-NAK от клиента. **Рекомендация:** Перепостить RECV сразу при `handle_recv`, до начала обработки. ### 3.17. Конфигурация `rdma_max_send/recv = 0` обрабатывается дефолтами, но `rdma_max_msg < OSD_PACKET_SIZE` не проверяется **Файл:** `src/client/messenger.cpp:320-331` Минимальные/максимальные границы не валидируются. `rdma_max_send = 1` даст депрессивную глубину; `rdma_max_msg < 128 байт` приведёт к assertion-фейлам в путях обработки заголовков. ### 3.18. NFS-RDMA: doesn't support multi-segment write chunks **Файл:** `src/nfs/nfs_proxy_rdma.cpp:962-976` Только 1 chunk с 1 segment в write chunk. Клиенты (не-Linux), пересылающие multi-segment, получают ERR_CHUNK. ### 3.19. NFS-RDMA: модифицирует входящее сообщение `rop->in_rdma_msg` при partial chunk write **Файл:** `src/nfs/nfs_proxy_rdma.cpp:641-646` `rdma_writes->entry.target.target_val[0].length = chunk_iov->iov_len` модифицирует структуру, которая должна быть immutable. Не баг в текущем коде, но потенциальная мина при будущих правках. ### 3.20. `out_of_range` exception в `cl->send_list.at(send_pos)` не обрабатывается **Файл:** `src/client/msgr_rdma.cpp:756-766` Использование `.at` при рассинхроне приводит к необработанному exception → `std::terminate`. **Рекомендация:** Или использовать `[]` (UB при OOB, но не throw) + assert, или ловить exception и `stop_client`. --- ## 4. ПРОБЛЕМЫ НИЗКОГО ПРИОРИТЕТА (Low) и стилистические ### 4.1. `is_global` определяется по факту наличия GID, а не по link_layer **Файл:** `src/client/msgr_rdma.cpp:481` Логика «GID не пустой → is_global=1» работает для RoCE (GID всегда не пустой), но для нативной IB с локальным LID-роутингом теоретически неверна. Лучше: `is_global = (link_layer == IBV_LINK_LAYER_ETHERNET || subnet_prefix != 0)`. ### 4.2. `hop_limit = 64` хардкод + комментарий «FIXME» **Файл:** `src/client/msgr_rdma.cpp:476` Не настраивается. Для multi-DC RDMA может не хватить. Дефолт OK. ### 4.3. `sl = 0` (service level) — отсутствует QoS **Файл:** `src/client/msgr_rdma.cpp:479` Без поддержки конфигурации SL нельзя интегрировать с PFC/DCQCN/lossless-классами трафика на современных RoCE-фабриках. ### 4.4. `pkey_index = 0` — фиксированно **Файл:** `src/client/msgr_rdma.cpp:448` Для мультипартиционных IB-кластеров может не подойти. ### 4.5. `timeout = 14` (~134 мс) — избыточно для локального DC Слишком долгое ожидание detection unreachable peer (~32 секунды до полного отказа QP). ### 4.6. Парсинг адреса в `msgr_rdma_address_t::from_string`: `%x` без ширины **Файл:** `src/client/msgr_rdma.cpp:48-49` `sscanf` без явной ширины для `qpn`, `psn` — недостаточная валидация. Не security-проблема (значения позже проверяются в `ibv_modify_qp`), но стилистически плохо. ### 4.7. GID-сериализация без разделителя между половинами **Файл:** `src/client/msgr_rdma.cpp:39-42` Читабельность. ### 4.8. `rdma_max_msg = 129*1024` — недокументированное магическое число **Файл:** `src/client/messenger.cpp:331` Без комментария «128 КБ payload + 1 КБ overhead». ### 4.9. `RDMA_MSGP` (deprecated в RFC 8166) отвергается без явной диагностики ### 4.10. `chunk_iov->iov_len == 0` для READ переписывает write chunk length=0 → проблема на iWARP ### 4.11. `ibv_get_cq_event` неэффективен: `read()` syscall на каждое событие Поток-event-channel + epoll → 1 syscall per notify. Альтернатива: hybrid polling (busy-spin + fallback на channel). ### 4.12. `fcntl` без проверки результата **Файлы:** многие места `fcntl(fd, F_SETFL, ... | O_NONBLOCK)` Возвращаемое значение не проверяется. В крайне редких случаях fcntl может вернуть `-1`. ### 4.13. RDMA-CM listener backlog не настраивается ### 4.14. NFS `next_recv_buf` — мёртвая переменная (всегда 0) **Файл:** `src/nfs/nfs_proxy_rdma.cpp:144, 802-803` Никогда не инкрементируется, всегда указывает на нулевой элемент `recv_buffers`. Читателю мисслидинг. ### 4.15. Отсутствие документации требований к сети В коде нет документации, что RDMA-сеть должна быть доверенной (нет аутентификации/шифрования RDMA). ### 4.16. WITH_RDMACM без WITH_RDMA — нерабочая компиляция **Файлы:** `messenger.h:47, 170, 257, 261, 309`, `msgr_rdma.h:5-8` WITH_RDMACM требует WITH_RDMA (типы из `msgr_rdma.h`), но `#error`-защиты нет. ### 4.17. `send_out_size = 0` в конце деструктора `~msgr_rdma_connection_t()` **Файл:** `src/client/msgr_rdma.cpp:99` Мёртвый код — присвоение полю в уже удаляемом объекте. ### 4.18. `accept_connections` дублирует строку `cl->peer_addr = addr;` **Файл:** `src/client/messenger.cpp:785-786` (это TCP, но рядом с RDMA логикой) ### 4.19. `init_recv_rdma` объявлен как `bool`, но всегда возвращает `true` (или умирает) **Файл:** `src/client/msgr_rdma.cpp:662-680` Возвращаемое значение бесполезно. См. 1.1. ### 4.20. `nfs_rdma_buf_t::len`, `mr` поля без копи-семантики/move-семантики Сейчас не вызывает проблем, но при копировании `nfs_rdma_buf_t` MR будет двойно зарегистрирован. --- ## 5. РЕКОМЕНДАЦИИ В ПРИОРИТЕТНОМ ПОРЯДКЕ ### Приоритет 1 — немедленно (Critical) 1. **Уйти от `exit(1)` в RDMA-путях** — заменить на `stop_client`/`stop_session`. Без этого OSD/NFS-прокси нельзя считать production-ready. (§1.1) 2. **Исправить порядок destruction в `~nfs_rdma_conn_t()`** — сначала QP, потом CQ. (§1.5) 3. **Исправить use-after-free в `handle_io`** после `client->stop()` — отложенное удаление. (§1.2) 4. **Очистка очередей перед `~nfs_rdma_conn_t()` / `rdma_malloc_destroy`** — убрать `assert`/`abort()`. (§1.3, §1.4) 5. **Лимиты на длины полей из недоверенных XDR**: `target_len`, `read_chunk_size`, `wr_count`. (§1.7, §1.8) 6. **Поменять порядок `dereg_mr` ↔ `free` в `~msgr_rdma_buf_t()`**. (§1.10) 7. **Use-after-free в `handle_rdmacm_events`**: проверять валидность `cmid` перед использованием. (§1.6) 8. **Сидировать `lrand48()` для PSN** или использовать `getrandom()`. (§1.9) 9. **Race в `rdmacm_set_conn_timeout` callback`**: использовать `find`, не `at`. (§1.13) 10. **Утечка `rdmacm_connecting_t` при разрушении мессенджера**. (§1.12) 11. **Доработка `cm_refs` или его удаление** (либо защита на уровне порядка деструкции). (§1.11, §2.8) ### Приоритет 2 — следующая итерация (High) 12. **Реализовать обработку `ibv_get_async_event`**. (§2.1) 13. **Поднять `min_rnr_timer`** до 12-16. (§2.2) 14. **Реализовать RDMA `NOMSG`** для совместимости с RFC 8166. (§2.3) 15. **Утечка `used_buffers` в NFS**. (§2.4) 16. **Поправить тип `send_out_size`** и проверки `__builtin_mul_overflow`. (§2.5) 17. **Лимиты в `handle_op_hdr`/`handle_reply_hdr`**. (§2.6) 18. **`rnr_retry = 6`** вместо бесконечного `7`. (§2.7) 19. **`rdma_max_send` default = 32-64**, документировать формулу. (§2.9) 20. **`recv_buf` через общий MR-pool**, header-first. (§2.10) 21. **Чистка двойного REJECTED в `handle_rdmacm_events`**. (§2.12) 22. **Запостить RECV до `rdma_accept`**. (§2.13) ### Приоритет 3 — рефакторинг (Medium) 23. Endianness для NFS magic, `remote_invalidate=false` по умолчанию. (§3.1, §3.2) 24. `max_send_sge > 1` + scatter-gather из iovec без `memcpy`. (§3.3, §3.4) 25. Capability-check для `IBV_WR_SEND_WITH_INV`. (§3.5) 26. `ibv_resize_cq` с верхним лимитом. (§3.6) 27. Clamp `max_send_wr/max_recv_wr` по device caps. (§3.7) 28. `IBV_SEND_INLINE` для маленьких сообщений. (§3.8) 29. iWARP-совместимость: `LOCAL_WRITE` для send MR, skip zero-byte RDMA_WRITE. (§3.9) 30. Хардкоды `responder_resources/initiator_depth = 1`. (§3.10) 31. Настраиваемый `rdma_listen` backlog. (§3.11) 32. `xdr_free` в `free_rdma_rpc_op`. (§3.12) 33. Обработать `wr_id = 4`. (§3.13) 34. Защита от бесконечной рекурсии `chunk_error`. (§3.14) 35. Валидация конфигурационных лимитов. (§3.17) ### Приоритет 4 — улучшения (Low) 36-50. Стилистика, мёртвый код, документация (см. §4). --- ## 6. ВЫВОД Реализация RDMA в Vitastor содержит **существенные дефекты надёжности и безопасности**, которые делают её непригодной для production-эксплуатации в нынешнем виде на узлах, обслуживающих недоверенный или большой объём трафика. В то же время архитектура (отдельный RDMA-мессенджер с фолбэком на TCP, поддержка RDMA-CM, отдельный путь для NFS-RDMA) выглядит разумной. Главные системные проблемы: - **Систематическое `exit(1)`** на временных RDMA-сбоях — самый яркий дефект, влияющий на все ситуации. - **Несколько потенциальных RCE/heap-corruption** в NFS-RDMA через невалидированные размеры от клиента. - **Незавершённая работа `cm_refs`** и связанные потенциальные use-after-free на ibv_context. - **Отсутствие наблюдения за асинхронными событиями устройства** — недостаточно reactive к проблемам сети. - **Производительность ниже возможностей оборудования** из-за хардкодных `rdma_max_send=8`, `max_send_sge=1` (NFS), отсутствия inline-SEND, лишних `memcpy`-копий. После закрытия Critical-проблем (§1) можно говорить о приемлемой работоспособности; после High (§2) — о приемлемой производительности и наблюдаемости. Medium/Low — задел для дальнейшего созревания.
maxpain changed title from RDMA implementation review using AI (Claude) to RDMA implementation review using AI (Claude 4.7) 2026-05-29 03:43:57 +03:00
Author

Переделаю с помощью Opus 4.8 + ultracode

Переделаю с помощью Opus 4.8 + ultracode
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vitalif/vitastor#69
No description provided.