Data integrity review using AI (Claude 4.7) v2 #67

Closed
opened 2026-05-12 08:05:49 +03:00 by maxpain · 5 comments

Аудит безопасности данных Vitastor

Дата аудита: 2026-05-12
Версия кода: master @ 4b7d0ce2 ("Add data device fsyncs during compaction")
Контекст: Аудит проведён в связи с использованием Vitastor в качестве слоя хранения для публичного коммерческого облака. Цель — выявить дефекты, способные привести к потере или повреждению клиентских данных в продакшене.
Метод: 11 параллельных агентов, каждый аудитировал свою подсистему путём полного построчного чтения исходников. Найденные критические дефекты затем повторно верифицированы прямым чтением кода.


TL;DR — самые опасные находки

# Подсистема Файл:строка Серьёзность Эффект
1 crc32c src/util/crc32c.c:413 CRITICAL Опечатка: проверка left_pad>0 вместо right_pad>0. CRC может быть рассчитан без учёта правого паддинга → ложные/пропущенные ошибки целостности при чтении.
2 Heap blockstore src/blockstore/blockstore_heap.cpp:1727-1742 CRITICAL punch_holes модифицирует bitmap/checksums существующей entry, но не пересчитывает wr->crc32c. После перезапуска OSD entry помечается как corrupt → потеря BIG_WRITE'а → потеря пользовательских данных.
3 OSD primary src/osd/osd_primary_subops.cpp:391-397 CRITICAL Для реплицированных пулов primary использует OSD_OP_SEC_WRITE_STABLE, но проверка read_error ловит только OSD_OP_SEC_WRITE. Partial-write на 2/3 репликах остаётся незамеченным, клиент тихо получает старые данные.
4 OSD primary src/osd/osd_primary_subops.cpp:167 + osd_primary.cpp:441-450 CRITICAL Для replicated stripe_num=0, поэтому stripes[0].osd_num перезаписывается на каждой реплике в цикле. Даже если #3 исправить, mark_partial_write пометит максимум 1 из N расходящихся реплик.
5 disk-tool src/disk_tool/disk_tool_resize.cpp:114 CRITICAL raw_resize() всегда return 0 — все ошибки шагов resize маскируются. Caller начинает затирать старые суперблоки → необратимая потеря OSD.
6 Cluster client src/client/cluster_client_wb.cpp:226 / 447 CRITICAL Underflow writeback_bytes при flush'е серии DELETE: += не считает DELETE-буферы, -= считает. Assert / hot loop в release-сборке → крах клиента и потеря writeback.
7 Cluster client src/client/cluster_client.cpp:1174-1180 CRITICAL После fatal-ошибки write CACHE_REPEATING-буфер не очищается; read из той же области в той же сессии возвращает данные несостоявшейся записи.
8 Cluster client src/client/cluster_client.cpp:1467-1483 HIGH/CRITICAL SYNC возвращает успех мгновенно, если все OSD из dirty_osds отключены. Данные ещё CACHE_REPEATING, при крахе клиента теряются.
9 Scrub src/osd/osd_scrub.cpp:488-522 CRITICAL При replicated голосовании в минорной группе помечается только канонический член класса эквивалентности. Остальные битые копии остаются как «валидные» → выдача битых данных клиенту + распространение порчи в recovery.
10 Scrub src/osd/osd_scrub.cpp:630 CRITICAL OOB-доступ в op_data->stripes[chunk.role] при EC-объектах с OUTDATED-чанками: размер массива stripe_count, но индексирует chunk.role ∈ [0, pg_size).
11 OSD peering src/osd/osd_peering_pg.cpp:387-402 + osd_primary.cpp:676-678 CRITICAL Объект со state == OBJ_CORRUPTED-only попадает в misplaced_objects через else-ветку, а remove_object_from_state не имеет ветки для CORRUPTED-only → runtime_error → крах OSD при обычной write/delete операции.
12 Heap blockstore src/blockstore/blockstore_init.cpp:310-352 CRITICAL После recheck-перезаписи метаблоков в init нет fsync(meta_fd) (как только что был добавлен для compaction в 4b7d0ce2). При сбое питания после старта но до флэша — потеря очистки garbage, повторные double-claim.
13 Monitor mon/mon.js:304, 802 CRITICAL save_last_clean() и update_total_stats() пишут в etcd без проверки LEASE на /mon/master. Старый монитор после потери lease перезаписывает данные нового → split-brain.

Полный перечень с воспроизведением, последствиями и рекомендациями — в разделах ниже.


1. Критические баги в blockstore (хранилище)

1.1. CRITICAL — crc32c_pad: опечатка left_pad вместо right_pad

Файл: src/util/crc32c.c:413

Код (подтверждён):

uint32_t crc32c_pad(uint32_t prev_crc, const void *buf, size_t len, size_t left_pad, size_t right_pad)
{
    ...
    while (right_pad >= 4096)
    {
        r = crc32c(r, zero_page, 4096);
        right_pad -= 4096;
    }
    if (left_pad > 0)              // <-- ОПЕЧАТКА: должно быть right_pad > 0
        r = crc32c(r, zero_page, right_pad);
    return r;
}

Описание: При left_pad == 0 остаток right_pad % 4096 не учитывается в CRC. Также возможна обратная асимметрия: при left_pad > 0 && right_pad == 0 crc32c(r, zero_page, 0) — безвредно, но семантически тоже неверно.

Сценарии воздействия:

  1. Чексумма с bitmap_granularity < csum_block_size: hint-биты в bitmap указывают, что часть csum-блока «нулевая». При расчёте csum через crc32c_pad правый «нулевой» паддинг должен войти в CRC, но из-за бага не входит.
  2. Если запись и чтение проходят через один и тот же crc32c_pad, баги взаимокомпенсируются → проверка проходит. Однако:
    • При миграции/перепаковке (например, через crc32c_iter в blockstore_heap.cpp:1161 — иной кодовый путь, который правильно учитывает паддинги) — заново вычисленная CRC не совпадёт с записанной.
    • Результат: валидные данные начинают помечаться как corrupt; в худшем случае recovery восстановит «правильную» (на самом деле потерянную) версию из плохой реплики.

Воспроизведение: csum_block_size > bitmap_granularity, частичная запись через blockstore v1 (src/blockstore/v1/write.cpp:184-200), либо любая ситуация, в которой crc32c_pad вызывается с left_pad=0, right_pad>0, right_pad<4096.

Рекомендация (1 символ): заменить на if (right_pad > 0).


1.2. CRITICAL — punch_holes не обновляет wr->crc32c в heap blockstore

Файл: src/blockstore/blockstore_heap.cpp:1727-1742

Код (подтверждён):

int blockstore_heap_t::punch_holes(heap_entry_t *wr, uint8_t *new_bitmap, uint8_t *new_csums, uint32_t *modified_block)
{
    assert(dsk->data_csum_type && dsk->csum_block_size > dsk->bitmap_granularity);
    assert(new_csums);
    uint32_t block_num = list_item(wr)->block_num;
    auto & inf = block_info.at(block_num);
    if (inf.is_writing)
    {
        return EAGAIN;
    }
    *modified_block = block_num;
    memcpy(wr->get_int_bitmap(this), new_bitmap, dsk->clean_entry_bitmap_size);
    memcpy(wr->get_checksums(this), new_csums, dsk->data_block_size/dsk->csum_block_size*(dsk->data_csum_type & 0xFF));
    return 0;
    // !!! wr->crc32c НЕ пересчитан !!!
}

Проверка grep показывает: calc_crc32c вызывается только в add_entry:1548. Ни punch_holes, ни add_compact (1700-1725) не пересчитывают crc32c существующей entry, в которой меняется содержимое.

Что произойдёт:

  • При компакции/flush с включённым perfect_csum_update (требует csum_block_size > bitmap_granularity) bitmap и checksums существующего BIG_WRITE entry модифицируются.
  • Метаблок пишется на диск (blockstore_flush.cpp:385).
  • При следующем старте OSD read_blocks (blockstore_heap.cpp:383-389) проверяет wr->crc32c != calc_crc32c() → mismatch → entry classified as corrupt.
    • skip_corrupted_meta_entries = false (по умолчанию) → OSD отказывается стартовать.
    • skip_corrupted_meta_entries = true → BIG_WRITE пропускается = полная потеря пользовательского блока данных (данные на disk-данных есть, но ссылки в метаданных нет → расцениваются как «свободное место» и могут быть перезаписаны).

Рекомендация: добавить wr->crc32c = wr->calc_crc32c(); в конце punch_holes. Также аудитировать любые in-place изменения существующих entries.


1.3. CRITICAL — Забыт fsync(meta_fd) после recheck-перезаписи метаблоков в init

Файл: src/blockstore/blockstore_init.cpp:310-345

Описание: После finish_recheck блокстор записывает «исправленные» метаблоки (с удалённым garbage) через io_uring_prep_writev. Никакого io_uring_prep_fsync(meta_fd, IORING_FSYNC_DATASYNC) после цикла нет. initialized = 10 выставляется немедленно.

Параллель: только что закрыт аналогичный баг для compaction — коммит 4b7d0ce2 "Add data device fsyncs during compaction (forgotten during initial heap store dev)". Здесь тот же класс ошибки в init-пути.

Последствия:

  • Если OSD отрабатывает recheck (т.е. был грязный shutdown), завершает init и сразу теряет питание до того, как страничный кэш ядра/write-cache диска успеют сброситься:
    • На следующем старте garbage entries снова обнаруживаются.
    • Возможна повторная double-claim ошибка (см. коммит caa70317), которая теперь срабатывает на разных entries и init_erase_double_claim начинает удалять валидные объекты.
  • При disable_meta_fsync=false (это режим по умолчанию) такого допускать нельзя.

Рекомендация: добавить fsync meta после цикла recheck_modified_blocks, аналогично compaction-fix.


1.4. HIGH — Use-after-free в recheck_buffer::free_entry

Файл: src/blockstore/blockstore_heap.cpp:864-892

free(li);                                     // освобождаем
modify_alloc(block_num, [&](heap_block_info_t & inf)
{
    ...
    for (auto it = inf.entries.begin(); it != inf.entries.end(); it++)
    {
        if (*it == li)                        // !!! freed pointer comparison !!!
            ...

C++ UB. На практике glibc обычно не переиспользует адрес немедленно, но в комбинации с jemalloc/tcmalloc/thread-local arenas возможна ложная коллизия → пропуск удаления entry из inf.entries или падение assert(found).

Рекомендация: сохранить копию pointer для сравнения до free(li), либо free(li) после modify_alloc.


1.5. HIGH — free_buffer_area(_, 0, 0) падает на zero-length small_write в init-пути

Файл: src/blockstore/blockstore_heap.cpp:712-722 (нет проверки if (wr->small().len > 0) в init_free_bad_entry, хотя такая проверка есть в аналогичных местах на строках 589 и 2001).

Последствие: multilist_alloc_t::do_free(0) падает на assert(!nexts[pos]) → крах OSD в init. OSD не может стартовать. Условие триггерится при init_erase_double_claim для EC-пула с zero-length small_writes (bump-версии без данных).


1.6. HIGH — Хрупкий инвариант assert(!inf.mod_lsn_to || inf.mod_lsn_to == next_lsn)

Файл: src/blockstore/blockstore_heap.cpp:1376, 1469

Если между двумя allocate_entry в один и тот же meta-block произошёл allocate_entry в другой block, next_lsn инкрементировался для чужого блока. Затем при возврате к исходному блоку inf.mod_lsn_to == old_value != next_lsn → ассерт. Падение OSD под обычной mixed-нагрузкой (big + small + intent).


1.7. MEDIUM — iterate_with_stable/iterate_compaction не skip garbage entries

Файл: src/blockstore/blockstore_heap.cpp:2030-2069, 2080-2159

В отличие от validate_object:464 и mark_garbage_up_to:1983, эти итераторы не пропускают is_garbage() entries. Reader через iterate_with_stable может выбрать garbage entry с location, уже переиспользованным новой записью → возврат wrong data клиенту.


1.8. CRITICAL — intent_write_allowed без проверки use_atomic_flag

Файл: src/blockstore/blockstore_write.cpp:55-110 / 459-462

Сценарий:

  • disable_meta_fsync=true, atomic_write_size=16384, use_atomic_flag=false.
  • intent_write_allowed пропускает запись (len <= atomic_write_size), но в resume_10 RWF_ATOMIC не ставится.
  • Torn write 16 КБ → испорчены данные внутри BIG_WRITE.
  • При disable_meta_fsync=true запись метаданных считается durable сразу → trim_lsn сжигает recheck-возможность.
  • Тихое необнаруживаемое повреждение данных в объекте.

Рекомендация: в intent_write_allowed добавить if (!dsk.use_atomic_flag) return false;.


2. Критические баги в OSD primary (репликация)

2.1. CRITICAL — mark_partial_write сломан для replicated пулов

Файл: src/osd/osd_primary_subops.cpp:391-397 (подтверждено)

Код:

if (opcode == OSD_OP_SEC_READ && (retval == -EIO || retval == -EDOM) ||
    opcode == OSD_OP_SEC_WRITE && retval != expected)
{
    ((osd_rmw_stripe_t*)subop->rmw_buf)->read_error = true;
}

Replicated primary использует OSD_OP_SEC_WRITE_STABLE (см. osd_primary_subops.cpp:204, 236), но проверка ловит только OSD_OP_SEC_WRITE. Поэтому read_error для replicated никогда не устанавливается, а значит mark_partial_write (osd_primary.cpp:432-453) никаких чанков как LOC_OUTDATED не пометит.

Сценарий потери данных (реалистичный):

  1. Реплика-3, ENOSPC на одном из устройств.
  2. WRITE: 2 реплики записали новую версию STABLE, 3-я вернула ENOSPC.
  3. read_error для 3-й не выставлен → mark_partial_write не помечает 3-ю как OUTDATED.
  4. Объект НЕ попадает в misplaced_objects / incomplete_objects.
  5. Следующий READ выбирает реплику по zero_read (свой OSD или первая ненулевая). Если выбрана отставшая реплика → клиент тихо получает старые данные.
  6. Следующий WRITE через RMW_READ от отстающей реплики возвращает старую fact_ver. target_ver = old+1 = уже записанная новая версия на других → EEXIST → drop соединений → re-peering → возможна более тяжёлая инконсистентность.

Рекомендация: добавить || opcode == OSD_OP_SEC_WRITE_STABLE в проверку.


2.2. CRITICAL — stripes[0] перезаписывается для каждой реплики

Файл: src/osd/osd_primary_subops.cpp:167, src/osd/osd_primary.cpp:441-450

submit_primary_subop_batch для replicated использует stripe_num = rep ? 0 : role, т.е. si = &stripes[0] для всех реплик. si->osd_num перезаписывается на каждой итерации цикла. После завершения stripes[0].osd_num хранит только последнюю реплику.

Даже если исправить #2.1, mark_partial_write сможет пометить максимум одну реплику из множества расходящихся.

Рекомендация: для replicated завести per-replica структуру ошибок (например, массив параллельный stripes), а не делить общий stripes[0].


2.3. CRITICAL — Отсутствует if (n_subops > 0) обёртка в continue_primary_sync для copies_to_delete

Файл: src/osd/osd_primary_sync.cpp:195-198

Коммит 5100f822 обернул аналогичные места, но пропустил именно этот путь:

else if (op_data->copies_to_delete)
{
    submit_primary_del_batch(cur_op, op_data->copies_to_delete, op_data->copies_to_delete_count);
resume_7:
    op_data->st = 7;
    return;

Если submit_primary_del_batch не создаст ни одного subop (рассогласование счётчиков copies_to_delete_after_sync_count и фактического содержимого), SYNC висит навечно. Последующие SYNC-ы блокируются друг другом → OSD выпадает из обслуживания записи.


2.4. HIGH — UB в free(op_data->snapshot_bitmaps)

Файл: src/osd/osd_primary_chain.cpp:193

op_data->snapshot_bitmaps указывает на офсет внутри одной большой calloc_or_die-аллокации op_data (osd_primary.cpp:113-129). free() на не-начале аллокации — UB; на glibc обычно abort("invalid pointer").

Триггер: chained-read для EC, когда extend_missing_stripes возвращает -1 (недостаточно живых OSD для восстановления). OSD аварийно завершается.


2.5. HIGH — pg_state_bits перезаписывается, теряется PG_HAS_CORRUPTED

Файл: src/osd/osd_primary.cpp:521-538

if (n_corrupted > 0) {
    pg_state_bits |= PG_HAS_CORRUPTED;      // |=
}
else if (n_roles < pg.pg_data_size) {
    pg_state_bits = PG_HAS_INCOMPLETE;      // = (затирает!)
}
...

pg.state не получает PG_HAS_CORRUPTED для первого corrupted объекта в degraded PG — оператор не видит сигнал, monitoring молчит.


2.6. HIGH — Утечка cur_op->buf при retry-чтения после mark_object_corrupted

Файл: src/osd/osd_primary.cpp:272, 297-303

При goto resume_0 старый cur_op->buf теряется без free(). В osd_primary_chain.cpp:59 это сделано корректно. Здесь — забыто. При множественных корраптингах → OOM OSD.


2.7. MEDIUM — Гонка между PG_REPEERING и in-flight WRITE

Файл: src/osd/osd_primary_write.cpp:225-232

PG-state проверяется один раз перед submit_primary_subops(SUBMIT_WRITE). Если PG переходит в PG_STOPPING|PG_REPEERING после запуска subops, операция продолжается. При быстрой смене primary возможна ситуация двух «пишущих» primary одновременно (split-brain в окне re-peering).


2.8. CRITICAL — Объект OBJ_CORRUPTED-only → крах OSD при write/delete

Файл: src/osd/osd_peering_pg.cpp:387-402 + osd_primary.cpp:676-678

Объект со state OBJ_CORRUPTED (без других флагов) попадает в misplaced_objects через else-ветку. remove_object_from_state использует цепочку else if для INCONSISTENT/INCOMPLETE/DEGRADED/MISPLACED, а для CORRUPTED-only-state выкидывает throw runtime_error("BUG: Invalid object state: ...").

Триггер:

  • EC-пул, объект имеет полный набор чистых чанков на cur_set + extra-чанк, оставшийся от rebalance.
  • Extra-чанк corrupts (сбой носителя).
  • Scrub помечает extra-чанк → add_object_to_set даёт obj_state = OBJ_CORRUPTED-only.
  • Объект сохранён в misplaced_objects.
  • Следующая обычная write/delete операция вызывает remove_object_from_state → throw → крах OSD.
  • При перезапуске сценарий повторяется → бесконечный crash-loop.

3. Критические баги в EC / RMW

3.1. CRITICAL — ec_check_combination модифицирует оригинальные bmp_buf стрипов

Файл: src/osd/osd_rmw.cpp:1198-1216 (внутри ec_find_good)

Функция использует bitmap_set непосредственно на оригинальных stripes[i].bmp_buf. После возврата вызывающий получает повреждённые bitmaps. При scrub с bitmap_size > 0 возможна потеря информации о sparse regions.


3.2. CRITICAL — ec_find_good не ловит ambiguity при «меньшем найденном первым»

Файл: src/osd/osd_rmw.cpp:1361-1378

if (found_valid.size() >= valid_chunks.size() && found_valid != valid_chunks)
    // ambiguity

Этот код ловит ambiguity ТОЛЬКО когда новый valid set ≤ предыдущего. Если первая комбинация дала меньший valid set, а вторая — больший, ambiguity молчаливо игнорируется И больший набор отбрасывается. При scrub_find_best = true (default) → scrub может принять неверный набор данных как «good» и репарировать остальные на его основе.


3.3. CRITICAL — Majority voting репарирует «правильную» копию из 2 испорченных

Файл: src/osd/osd_scrub.cpp:493-538 + документация на scrub_find_best

Если 3 реплики и 2 одинаково испорчены (тот же дефект на 2 HDD с одинаковой прошивкой) — majority выбирает «плохую» версию как эталон, помечает хорошую копию как corrupted, и recovery затем перезаписывает хорошую копию плохими данными. Все 3 реплики становятся битыми.

Документация (docs/config/osd.en.md) явно признаёт сценарий, но scrub_find_best=true остаётся default. Для коммерческого облака рекомендуется выставлять scrub_find_best=false глобально.


3.4. CRITICAL — OOB-доступ op_data->stripes[chunk.role] для EC с OUTDATED-чанками

Файл: src/osd/osd_scrub.cpp:630

Массив stripes имеет размер stripe_count (= число не-OUTDATED чанков), а chunk.role может быть [0, pg_size). Для EC объектов с OUTDATED-чанками возникает OOB-чтение → ложное снятие/установка LOC_CORRUPTED на основе случайных байт.


3.5. HIGH — reconstruct_stripes_ec без assert на корректное число чанков

Файл: src/osd/osd_rmw.cpp:410-419

Если функция вызвана при количестве доступных чанков < pg_minsize (логическая ошибка выше по стеку), цикл сбора data_ptrs не заполнит все позиции, и ISA-L использует неинициализированные указатели → чтение мусора → реконструкция из мусора → записанные мусорные данные при recovery.


3.6. HIGH — jerasure unaligned branch расширяет цикл за pg_minsize

Файл: src/osd/osd_rmw.cpp:556-565

for (role = 0; role < pg_size; role++) отличается от aligned-ветки (role < pg_minsize). При missing parity с read_end != 0 используется decoding_matrix + (role*pg_minsize) за пределами выделенного массива → потенциальный OOB read и неверная реконструкция.


4. Критические баги в client / write-back cache

4.1. CRITICAL — Underflow writeback_bytes при flush последовательных DELETE

Файл: src/client/cluster_client_wb.cpp:226, 447 (подтверждено)

writeback_bytes += is_del ? 0 : op->len;   // строка 226: DELETE не добавляет
...
writeback_bytes -= off - from_it->first.stripe;  // строка 447: вычитает полностью
assert(writeback_queue_size > 0 || !writeback_bytes);

При series соседних OSD_OP_DELETE (которые группируются в одну batch) writeback_bytes уходит в underflow uint64_t. В debug-сборке падает assert → потеря всех unflushed данных. В release (assert disabled) while (wb->writeback_bytes > client_max_buffered_bytes ...) в execute_internal:784 превращается в бесконечный цикл на следующей операции записи.


4.2. CRITICAL — Heap overflow в copy_write при некорректном iov

Файл: src/client/cluster_client_wb.cpp:247-258

while (iov_idx < op->iov.count)
{
    auto & iov = op->iov.buf[iov_idx];
    assert(pos + iov.iov_len <= len);
    memcpy(buf + pos, iov.iov_base, iov.iov_len);
    ...
}

buf выделен размером op->len. Если caller передал op->iov с суммарной длиной > op->len, в release-сборке (assert исчез) memcpy пишет за границей буфера → heap corruption.


4.3. CRITICAL — Фантомный read данных несостоявшегося write

Файл: src/client/cluster_client.cpp:1174-1180

При fatal-error пути (-EROFS, -EACCES, -EINVAL и т.п.) mark_flush_written НЕ вызывается, и CACHE_REPEATING-буфер остаётся в dirty_buffers навсегда. Последующий read того же региона в той же сессии копирует данные из этого буфера через read_from_cache:515. Application получает данные write'а, который вернул ошибку.

Это нарушает базовую семантику и в коммерческом облаке потенциально приводит к скрытому повреждению на уровне приложения клиента (например, файловые системы поверх RBD).


4.4. HIGH/CRITICAL — SYNC возвращает успех при disconnected OSD

Файл: src/client/cluster_client.cpp:1467-1483

При disconnect OSD между write OK и client SYNC:

  1. Buffer становится CACHE_REPEATING (через repeat_ops_for после disconnect).
  2. SYNC удаляет OSD из dirty_osds (он не в msgr.osd_peers).
  3. dirty_osds становится пустым → SYNC возвращает 0 мгновенно.
  4. Application считает данные на диске, отпускает их.
  5. Если клиент крашится до завершения flush_buffers re-send — данные потеряны.

Нарушение семантики fsync.


4.5. HIGH — TOCTOU в send_sync

Файл: src/client/cluster_client.cpp:1536

msgr.osd_peers.at(part->osd_num) бросает исключение, если в callback'ах внутри цикла отправки произошёл stop_client. → необработанный exception → крах клиента с возможным неконсистентным writeback.


4.6. HIGH — Reentrant stop_client в handle_op_part

Файл: src/client/cluster_client.cpp:1611-1614

Авторами помечено FIXME (// FIXME postpone such things to set_immediate here to avoid bugs). При нескольких parts на одного primary с -EPIPE возможен двойной вызов continue_sync/rw на одну op и нарушение инварианта inflight_count.


4.7. HIGH — bitmap_buf не обнуляется при reslice

Файл: src/client/cluster_client.cpp:1263-1269

Обнуление выполняется только если bitmap_mem > bitmap_buf_size. При reslice без расширения область part_bitmaps хранит биты прошлого slice'а. В сочетании с server'ом, который возвращает короткий bitmap, → ложные «1» в bitmap для snapshot/chain reads → пропуск обращения к parent inode → incorrect read.


5. Критические баги в scrub

5.1. CRITICAL — Replicated scrub помечает только канонического члена минорной группы

Файл: src/osd/osd_scrub.cpp:488-522

Цикл votes[eq_to[role]] заполняет только канонические индексы каждого класса эквивалентности; цикл маркировки итерирует по role и видит votes[role]==0 для не-канонических → они не помечаются как LOC_CORRUPTED.

Эффект: в группе из 2 «плохих» копий с одинаковыми данными после scrub'а одна (каноническая) помечена, вторая — нет. При следующем read primary может выбрать неотмеченную плохую копию (по zero_read) → клиент получает битые данные. При recovery read_target включает плохую копию → распространение порчи.


5.2. CRITICAL — OOB stripes[chunk.role] при OUTDATED-чанках

См. §3.4.


5.3. CRITICAL — Majority voting с scrub_find_best=true

См. §3.3.


5.4. HIGH — Scrub отключён для clean-объектов в degraded PG

Файл: src/osd/osd_scrub.cpp:384-393, 426

Для clean-объекта stripes создаются из cur_set, в котором может быть osd_num=0 (отключённый OSD). submit_primary_subop для osd_num=0 немедленно возвращает -EPIPE. errcode != EIO/EDOM → continue_primary_scrub (строки 663-665) делает finish_op без scrub_check_results. Scrub фактически не работает ни для одного clean-объекта в любом PG с хоть одним выпавшим OSD.

В реальном кластере это окно может длиться часами/днями (recovery). Silent corruption за это время не выявляется.


5.5. HIGH — get_object_osd_set игнорирует PG_HAS_CORRUPTED/INCONSISTENT

Файл: src/osd/osd_primary.cpp:184-211

Не проверяет PG_HAS_INCONSISTENT/CORRUPTED — если PG содержит только corrupted/inconsistent объекты (без degraded/misplaced/incomplete), возвращается cur_set (включая corrupted-OSD), а не read_target → re-scrub читает с corrupted-источника.


5.6. LOW — Hang scrub при stripe_count == 0

Файл: src/osd/osd_scrub.cpp:426-433

Если все чанки объекта помечены LOC_OUTDATED, n_subops=0, но op_data->st = 1 уже выставлен. Никто не разбудит. Слот в scrub_queue_depth блокируется навсегда.


6. Критические баги в peering/cluster

6.1. CRITICAL — OBJ_CORRUPTED-only объект → крах OSD

См. §2.8.


6.2. HIGH — Memory leak в remove_object_from_state recheck path

Файл: src/osd/osd_primary.cpp:613-622

При recheck_state == NULL и (*object_state)->ref_count после декремента == 0 entry в state_dict не очищается.


6.3. MEDIUM — drop_dirty_pg_connections не вызывается в else-ветке repeer_pg

Файл: src/osd/osd_peering.cpp:175-193

При переходе PG_ACTIVE → PG_REPEERING без start_pg_peering, dirty_pg_connections не сбрасываются. Клиенты с unsynced writes получают задержки/таймауты.


7. Критические баги в monitor / etcd

7.1. CRITICAL — save_last_clean() и update_total_stats() без проверки LEASE

Файл: mon/mon.js:304, 802

В отличие от save_pg_config():681, эти транзакции не проверяют LEASE на /mon/master. Старый монитор после потери lease может перезаписать last_clean_pgs устаревшими данными, что повредит логику generate_pool_pgs нового мастера → лишние data movements, потенциально incorrect placement.


7.2. CRITICAL — antietcd_adapter.become_master() без CAS

Файл: mon/antietcd_adapter.js:126-148

В отличие от etcd_adapter.become_master, проверка CREATE 0 отсутствует. Безусловная запись /mon/master. При кратковременном split-brain в antietcd raft два «лидера» могут оба перезаписать ключ.


7.3. HIGH — schedule_next_recheck_at(): таймер срабатывает мгновенно

Файл: mon/mon.js:722

setTimeout(() => { ... }, now - this.next_recheck_at);

now - next_recheck_at отрицательно/нулевое (next в будущем) → таймер срабатывает сразу, ещё и без * 1000. Политика osd_out_time (выкидывание OSD из дерева через 600 сек) фактически не работает корректно. PG висят в degraded дольше → при следующей потере OSD возможен переход в incomplete.


7.4. HIGH — Use-after-free в etcd retry-таймерах

Файл: src/client/etcd_state_client.cpp:151-155 и аналогичные в osd_cluster.cpp

Лямбды захватывают this. Деструктор etcd_state_client_t не отменяет произвольные retry-таймеры → при разрушении объекта (NFS proxy, dynamic reconfiguration) таймер триггерится с уже удалённым this.


7.5. HIGH — etcd_watch_revision_* устаревает при etcd_watches_initialised < ETCD_TOTAL_WATCHES

Файл: src/client/etcd_state_client.cpp:418-442

События watch обрабатываются сразу, но etcd_watch_revision_* обновляется только после полной инициализации всех watches. При обрыве/reconnect возможны дублирующие обработки событий, в частности пропуск on_change_pg_state_hook для зависших операций.


7.6. HIGH — clean_nonexistent_pgs не убирает удалённые inode'ы

Файл: src/client/etcd_state_client.cpp:736 / 770-822

При reload через compaction inode_config не очищается. Клиент пишет в удалённый inode → -EPIPE/-ENOENT. Не data corruption, но liveness-проблема.


7.7. MEDIUM — reshard_continue() UB на пустом векторе

Файл: src/osd/osd_cluster.cpp:803-810

reshard_pools[0] без проверки .empty() после goto-цикла.


8. Критические баги в messenger (TCP / RDMA)

8.1. HIGH — Утечка zc_free_list для партий без MSGR_SENDP_FREE

Файл: src/client/msgr_send.cpp:295-340

Если партия (типичная для клиента) не содержит FREE-операций, NULL-маркер всё равно добавляется. Второе уведомление (prev=true) удаляет операции только при i > 0 — NULL-маркер не убирается, и последующие операции с FREE «застревают» за чужими NULL-маркерами. Долгоживущее соединение OSD → OOM → крах OSD.


8.2. MEDIUM — Отсутствие валидации длин в handle_op_hdr

Файл: src/client/msgr_receive.cpp:264-346

sec_rw.len, attr_len, sec_stab.len, json_len принимаются без проверки и сразу передаются в malloc_or_die. При UINT32_MAX процесс падает через exit(1). DoS, но не data corruption. В trusted cluster не критично; при компрометации хотя бы одного OSD-узла — критично.


8.3. MEDIUM — bitmap_len короче ожидаемого → mixed-state в op->bitmap

Файл: src/client/msgr_receive.cpp:366-380

Если OSD возвращает bitmap короче op->bitmap_len, в op->bitmap остаются неинициализированные/устаревшие байты. При EC-recovery это приведёт к неверной интерпретации stripes → клиент получает corrupted data. Требует ошибки сервера, защиты в клиенте нет.


8.4. LOW — Ping-FIXME при RDMA-transition

Файл: src/client/msgr_receive.cpp:78-81 (FIXME: Ignore pings during RDMA state transition).

PING-reply пропускается → keepalive таймаут → ложный дисконнект и retry.


9. Критические баги в disk_tool

9.1. CRITICAL — raw_resize() всегда возвращает 0

Файл: src/disk_tool/disk_tool_resize.cpp:114 (подтверждено)

ret:
    free_new_meta();
    return 0;     // ВСЕ ошибки маскируются

Вызывающий код (disk_tool_resize_auto.cpp:111, disk_tool_upgrade.cpp:134) при «успехе» начинает:

  1. Очищать старые суперблоки (clear_osd_superblock).
  2. Записывать новые суперблоки.

При реальной ошибке resize данные могут быть частично перемещены / журнал и метаданные побиты, но старый суперблок уже стёрт. OSD не сможет стартовать; восстановление требует bare-metal вмешательства.

Рекомендация (1 символ): return r;.


9.2. CRITICAL — write_osd_superblock / clear_osd_superblock / write_zero без fsync

Файл: src/disk_tool/disk_tool_udev.cpp:141, 423-427, src/disk_tool/disk_tool_utils.cpp:278-300

После записи суперблока просто close(fd) без fsync. При write-back кэше диска (норма!) и потере питания:

  • prepare: суперблок «записан», но не на пластинах → OSD не стартует после reboot.
  • resize_data: новые суперблоки могут пропасть, но физическое перемещение данных уже произошло.
  • purge: суперблоки «очищены», но кэш может их вернуть — старый OSD может попытаться зарегистрироваться обратно.

9.3. CRITICAL — Не сбрасывается состояние KV-БД при close/open

Файл: src/kv/kv_db.cpp:680-699, 506-546

close() сбрасывает только block_cache.clear() и known_versions.clear(). Остаются block_levels, allocating_blocks, new_versions и т.д. При reopen на другой inode_id confirmed=true allocating blocks содержат смещения от предыдущего inode → новые записи в новый inode перепишут существующие KV-блоки на старте.


9.4. HIGH — Опечатка allocating_block_pos вместо next в alloc_block

Файл: src/kv/kv_db.cpp:712

if (allocating_blocks[allocating_block_pos].offset != UINT64_MAX && ... )

Должно быть allocating_blocks[next].offset. Лишние allocating blocks создаются, фрагментация растёт.


9.5. HIGH — Double-close при общем fd для journal и meta

Файл: src/disk_tool/disk_tool_meta.cpp:229-239

После close(meta_fd); meta_fd = -1; проверка journal_fd != meta_fd всегда true → double-close. В однопоточной утилите безвредно (EBADF), но при добавлении многопоточности → race с чужим fd.


9.6. HIGH — Орфанные KV-блоки при split-сбое root

Файл: src/kv/kv_db.cpp:1856-1922

clear_block использует version=0, что в CAS возвращает -EEXIST для уже-аллоцированных inode_block. Орфанные блоки никогда не очищаются → утечка пространства KV-индекса.


10. Util / Misc

10.1. HIGH — SQE128 partial init в ringloop

Файл: src/util/ringloop.cpp:93, 184-200

*sqe = { 0 }; обнуляет 64 байта, реальный SQE128 — 128 байт. restore() и space_left() используют >> sqe_shift, но sqe_tail инкрементируется на 1. Сейчас активируется только в ublk_server control path → не corruption, но мина при будущем использовании SQE128 в data path.

10.2. MEDIUM — readv_blocking infinite loop при EOF

Файл: src/util/rw_blocking.cpp:60-93

r == 0 от readv не обрабатывается → бесконечный цикл. Используется в fio_sec_osd.cpp (тестовая утилита), не в OSD.

10.3. LOW — free() C++ объектов в ~ring_loop_t()

Не вызывает деструкторы std::function callbacks → потенциальный memory leak.


11. Что НЕ найдено / выглядит корректно

Аудит подтвердил корректность:

  • Порядок fsync для BIG_WRITE: data → fsync → meta.
  • Recheck SMALL_WRITE/INTENT_WRITE по CRC32C при инициализации.
  • submit_fsyncs выбор FD.
  • completed_lsn в суперблоке через RWF_DSYNC.
  • mark_completed_lsns для out-of-order meta-блоков.
  • Защита PG locks через sec_check_pg_lock + lease.
  • Stabilize/rollback ordering per-OSD через TCP serialization.
  • Сериализация per-oid write через write_queue (для не-scrub операций).

Эти места выглядят надёжно реализованными.


12. Приоритизированный список действий

Срочно исправить перед запуском в продакшен

  1. src/util/crc32c.c:413 — заменить if (left_pad > 0) на if (right_pad > 0).
  2. src/blockstore/blockstore_heap.cpp:1727-1742 — добавить wr->crc32c = wr->calc_crc32c(); в конце punch_holes. Дополнительно: проверить, что аналогичная пересчёт делается в add_overwrite_in_place и других in-place модификаторах.
  3. src/blockstore/blockstore_init.cpp:310-352 — добавить io_uring_prep_fsync(meta_fd, IORING_FSYNC_DATASYNC) после цикла перезаписи recheck_modified_blocks (если !disable_meta_fsync && !readonly).
  4. src/osd/osd_primary_subops.cpp:391-397 — добавить OSD_OP_SEC_WRITE_STABLE в проверку read_error.
  5. src/osd/osd_primary_subops.cpp:167 + osd_primary.cpp:441-450 — переработать mark_partial_write для replicated: хранить per-replica статус ошибки отдельно, а не shared stripes[0].
  6. src/osd/osd_primary_sync.cpp:195-198 — обернуть путь copies_to_delete в if (n_subops > 0).
  7. src/osd/osd_primary_chain.cpp:193 — убрать free(op_data->snapshot_bitmaps) (память освобождается с op_data).
  8. src/osd/osd_peering_pg.cpp:387-402 — добавить ветку для OBJ_CORRUPTED-only состояния, либо обработать его в remove_object_from_state.
  9. src/osd/osd_primary.cpp:521-538 — заменить = на |= для всех веток pg_state_bits (теряется PG_HAS_CORRUPTED).
  10. src/osd/osd_scrub.cpp:488-522 — итерировать маркировку по eq_to[role]-классу, помечая все копии в классе.
  11. src/osd/osd_scrub.cpp:630 — индексировать массив stripes по позиции в нём, а не по chunk.role (или через map role → stripe index).
  12. src/osd/osd_scrub.cpp:384-393 — пропускать osd_num=0 чанки в submit_scrub_subops для clean-объектов (либо помечать их not_exists=true без отправки).
  13. src/disk_tool/disk_tool_resize.cpp:114 — заменить return 0; на return r;.
  14. src/disk_tool/disk_tool_udev.cpp:141, 423 + disk_tool_utils.cpp:278 — добавить fsync(fd) перед close(fd) для всех записей суперблока и write_zero.
  15. src/client/cluster_client_wb.cpp:447 — writeback_bytes -= должно учитывать только не-DELETE буферы; либо += на 226 должен += op->len независимо от is_del.
  16. src/client/cluster_client.cpp:1174-1180 — при fatal-error вызывать wb->mark_flush_written(op->inode, op->offset, op->len, op->flush_id) до erase_op.
  17. src/client/cluster_client.cpp:1475-1483 — при удалении OSD из dirty_osds проверять наличие CACHE_REPEATING-буферов на тот же OSD; не возвращать SYNC OK, пока re-flush не завершится.
  18. mon/mon.js:304, 802 — добавить compare: [{ target:'LEASE', lease: etcd_lease_id, key: ... '/mon/master' }] в обе транзакции.
  19. mon/antietcd_adapter.js:141 — добавить compare: [{ target:'CREATE', create_revision:0, key:'/mon/master' }].
  20. mon/mon.js:722 — заменить setTimeout(cb, now - next) на setTimeout(cb, (next - now) * 1000).

Важно — в ближайшем релизе

  1. intent_write_allowed (blockstore_write.cpp:55-110) — добавить if (!dsk.use_atomic_flag) return false;.
  2. iterate_with_stable / iterate_compaction — пропускать is_garbage() entries.
  3. recheck_buffer::free_entry — переставить порядок free(li) и modify_alloc.
  4. init_free_bad_entry — проверка if (wr->small().len > 0) перед free_buffer_area.
  5. osd_primary.cpp:303 — free(cur_op->buf); cur_op->buf = NULL; перед goto resume_0.
  6. Default scrub_find_best = false для коммерческого окружения (см. §3.3).
  7. SQE128 ringloop — синхронизировать инкременты sqe_tail / space_left с реальной семантикой.
  8. cluster_client deструктор — auto-flush для безопасности.

Защита-в-глубину

  1. Защита от reentrant stop_client в handle_op_part (FIXME уже стоит).
  2. Валидация длин в handle_op_hdr — отвергать с -EINVAL вместо exit(1).
  3. Per-FD retry-таймеры в etcd_state_client с регистрацией для отмены в деструкторе.
  4. Обработка OBJ_CORRUPTED в get_object_osd_set (включить в проверку маски).

13. Замечания об устройстве кода и о методе аудита

  1. Большинство «строгих» asserts в blockstore (assert(res==0), assert(li_entry_type == cwr_entry_type) и т.п.) дают защиту fail-stop, но иногда срабатывают на сценариях, которые могли бы возникнуть, если бы вышестоящий слой OSD когда-нибудь нарушил инвариант. В долгоживущем продакшене такие assert'ы — мины: их активация = полный downtime OSD без возможности recovery.
  2. Recent commit history (4b7d0ce2, 2a6ce356, f6e07f57, 04fe7eb2, 21f712ad, f271c845, fe8b1fe0, 1ec963e4, 5100f822, 87a52307, 0c5e6d43, b2780874, caa70317) — большой поток bugfix'ов конкретно в новом heap-блокстoре и в путях fsync/durability. Это сигнал: код только что стабилизируется. Для коммерческого облака с гарантиями данных рекомендуется:
    • Развёрнуть текущую версию только на стенде, прогнать её под продакшен-нагрузкой и сбойными сценариями (kill -9 OSD, отключение питания узла, потеря NIC, чтения во время компакции, scrub во время writeы) минимум 4-6 недель.
    • Параллельно отправить найденные находки разработчику для исправления.
    • Не запускать на heap-blockstore с perfect_csum_update=true до исправления #1.2.
  3. scrub_find_best=true (default) — для публичного облака необходимо переключить на false, чтобы исключить риск «починки» хорошей копии 2 одинаково-битыми (см. §3.3).
  4. Vitastor требует доверенной cluster network: messenger не имеет аутентификации/HMAC. Сетевая безопасность должна обеспечиваться отдельно (изоляция VLAN, eBPF firewall).

14. Приложение: что было проверено

Аудит охватил полное чтение и анализ следующих файлов (всего ≈25.5 KLOC C++ кода + JS-монитор):

  • src/blockstore/: blockstore.cpp/h, blockstore_disk.cpp/h, blockstore_flush.cpp/h, blockstore_heap.cpp/h, blockstore_impl.cpp/h, blockstore_init.cpp/h, blockstore_open.cpp, blockstore_read.cpp, blockstore_stable.cpp, blockstore_sync.cpp, blockstore_write.cpp, multilist.cpp/h, ondisk_formats.h
  • src/osd/: osd.cpp/h, osd_cluster.cpp, osd_flush.cpp, osd_peering.cpp, osd_peering_pg.cpp/h, osd_primary.cpp/h, osd_primary_chain.cpp, osd_primary_describe.cpp, osd_primary_subops.cpp, osd_primary_sync.cpp, osd_primary_write.cpp, osd_rmw.cpp/h, osd_rmw_test.cpp, osd_scrub.cpp, osd_secondary.cpp
  • src/client/: cluster_client.cpp/h, cluster_client_impl.h, cluster_client_list.cpp, cluster_client_wb.cpp, etcd_state_client.cpp/h, http_client.cpp/h, messenger.cpp/h, msgr_op.cpp/h, msgr_receive.cpp, msgr_send.cpp, msgr_stop.cpp, msgr_rdma.cpp/h, msgr_rdmacm.cpp, osd_ops.cpp/h, pg_states.cpp/h
  • src/util/: crc32c, ringloop, timerfd_manager, allocator, rw_blocking, epoll_manager, sha256, addr_util
  • src/disk_tool/: prepare, resize, resize_auto, journal, meta, udev, upgrade, utils, discard, simple_offsets
  • src/kv/: kv_db
  • mon/: mon.js, antietcd_adapter.js, etcd_adapter.js, pg_gen.js, pg_utils.js

Подтверждены прямым повторным чтением:

  • crc32c_pad left_pad/right_pad баг (CRITICAL #1)
  • punch_holes без crc32c (CRITICAL #2)
  • mark_partial_write для SEC_WRITE_STABLE (CRITICAL #3)
  • raw_resize всегда 0 (CRITICAL #5)
  • writeback_bytes underflow для DELETE (CRITICAL #6)

Все приведённые номера строк актуальны для коммита 4b7d0ce2.

# Аудит безопасности данных Vitastor **Дата аудита:** 2026-05-12 **Версия кода:** master @ `4b7d0ce2` ("Add data device fsyncs during compaction") **Контекст:** Аудит проведён в связи с использованием Vitastor в качестве слоя хранения для публичного коммерческого облака. Цель — выявить дефекты, способные привести к потере или повреждению клиентских данных в продакшене. **Метод:** 11 параллельных агентов, каждый аудитировал свою подсистему путём полного построчного чтения исходников. Найденные критические дефекты затем повторно верифицированы прямым чтением кода. --- ## TL;DR — самые опасные находки | # | Подсистема | Файл:строка | Серьёзность | Эффект | |---|------------|-------------|-------------|--------| | 1 | crc32c | `src/util/crc32c.c:413` | **CRITICAL** | Опечатка: проверка `left_pad>0` вместо `right_pad>0`. CRC может быть рассчитан без учёта правого паддинга → ложные/пропущенные ошибки целостности при чтении. | | 2 | Heap blockstore | `src/blockstore/blockstore_heap.cpp:1727-1742` | **CRITICAL** | `punch_holes` модифицирует bitmap/checksums существующей entry, но не пересчитывает `wr->crc32c`. После перезапуска OSD entry помечается как corrupt → потеря BIG_WRITE'а → потеря пользовательских данных. | | 3 | OSD primary | `src/osd/osd_primary_subops.cpp:391-397` | **CRITICAL** | Для реплицированных пулов primary использует `OSD_OP_SEC_WRITE_STABLE`, но проверка `read_error` ловит только `OSD_OP_SEC_WRITE`. Partial-write на 2/3 репликах остаётся незамеченным, клиент тихо получает старые данные. | | 4 | OSD primary | `src/osd/osd_primary_subops.cpp:167` + `osd_primary.cpp:441-450` | **CRITICAL** | Для replicated `stripe_num=0`, поэтому `stripes[0].osd_num` перезаписывается на каждой реплике в цикле. Даже если #3 исправить, mark_partial_write пометит максимум 1 из N расходящихся реплик. | | 5 | disk-tool | `src/disk_tool/disk_tool_resize.cpp:114` | **CRITICAL** | `raw_resize()` всегда `return 0` — все ошибки шагов resize маскируются. Caller начинает затирать старые суперблоки → необратимая потеря OSD. | | 6 | Cluster client | `src/client/cluster_client_wb.cpp:226 / 447` | **CRITICAL** | Underflow `writeback_bytes` при flush'е серии DELETE: `+=` не считает DELETE-буферы, `-=` считает. Assert / hot loop в release-сборке → крах клиента и потеря writeback. | | 7 | Cluster client | `src/client/cluster_client.cpp:1174-1180` | **CRITICAL** | После fatal-ошибки write CACHE_REPEATING-буфер не очищается; read из той же области в той же сессии возвращает данные **несостоявшейся** записи. | | 8 | Cluster client | `src/client/cluster_client.cpp:1467-1483` | **HIGH/CRITICAL** | SYNC возвращает успех мгновенно, если все OSD из `dirty_osds` отключены. Данные ещё CACHE_REPEATING, при крахе клиента теряются. | | 9 | Scrub | `src/osd/osd_scrub.cpp:488-522` | **CRITICAL** | При replicated голосовании в минорной группе помечается только канонический член класса эквивалентности. Остальные битые копии остаются как «валидные» → выдача битых данных клиенту + распространение порчи в recovery. | | 10 | Scrub | `src/osd/osd_scrub.cpp:630` | **CRITICAL** | OOB-доступ в `op_data->stripes[chunk.role]` при EC-объектах с OUTDATED-чанками: размер массива `stripe_count`, но индексирует `chunk.role ∈ [0, pg_size)`. | | 11 | OSD peering | `src/osd/osd_peering_pg.cpp:387-402` + `osd_primary.cpp:676-678` | **CRITICAL** | Объект со state == `OBJ_CORRUPTED`-only попадает в `misplaced_objects` через else-ветку, а `remove_object_from_state` не имеет ветки для CORRUPTED-only → `runtime_error` → крах OSD при обычной write/delete операции. | | 12 | Heap blockstore | `src/blockstore/blockstore_init.cpp:310-352` | **CRITICAL** | После recheck-перезаписи метаблоков в init нет `fsync(meta_fd)` (как только что был добавлен для compaction в `4b7d0ce2`). При сбое питания после старта но до флэша — потеря очистки garbage, повторные double-claim. | | 13 | Monitor | `mon/mon.js:304, 802` | **CRITICAL** | `save_last_clean()` и `update_total_stats()` пишут в etcd **без** проверки `LEASE` на `/mon/master`. Старый монитор после потери lease перезаписывает данные нового → split-brain. | > Полный перечень с воспроизведением, последствиями и рекомендациями — в разделах ниже. --- ## 1. Критические баги в blockstore (хранилище) ### 1.1. CRITICAL — `crc32c_pad`: опечатка `left_pad` вместо `right_pad` **Файл:** `src/util/crc32c.c:413` **Код (подтверждён):** ```c uint32_t crc32c_pad(uint32_t prev_crc, const void *buf, size_t len, size_t left_pad, size_t right_pad) { ... while (right_pad >= 4096) { r = crc32c(r, zero_page, 4096); right_pad -= 4096; } if (left_pad > 0) // <-- ОПЕЧАТКА: должно быть right_pad > 0 r = crc32c(r, zero_page, right_pad); return r; } ``` **Описание:** При `left_pad == 0` остаток `right_pad % 4096` не учитывается в CRC. Также возможна обратная асимметрия: при `left_pad > 0 && right_pad == 0` `crc32c(r, zero_page, 0)` — безвредно, но семантически тоже неверно. **Сценарии воздействия:** 1. Чексумма с `bitmap_granularity < csum_block_size`: hint-биты в bitmap указывают, что часть csum-блока «нулевая». При расчёте csum через `crc32c_pad` правый «нулевой» паддинг должен войти в CRC, но из-за бага не входит. 2. Если запись и чтение проходят через один и тот же `crc32c_pad`, баги взаимокомпенсируются → проверка проходит. Однако: - При **миграции/перепаковке** (например, через `crc32c_iter` в `blockstore_heap.cpp:1161` — иной кодовый путь, который правильно учитывает паддинги) — заново вычисленная CRC не совпадёт с записанной. - Результат: валидные данные начинают помечаться как corrupt; в худшем случае recovery восстановит «правильную» (на самом деле потерянную) версию из плохой реплики. **Воспроизведение:** `csum_block_size > bitmap_granularity`, частичная запись через blockstore v1 (`src/blockstore/v1/write.cpp:184-200`), либо любая ситуация, в которой `crc32c_pad` вызывается с `left_pad=0, right_pad>0, right_pad<4096`. **Рекомендация (1 символ):** заменить на `if (right_pad > 0)`. --- ### 1.2. CRITICAL — `punch_holes` не обновляет `wr->crc32c` в heap blockstore **Файл:** `src/blockstore/blockstore_heap.cpp:1727-1742` **Код (подтверждён):** ```cpp int blockstore_heap_t::punch_holes(heap_entry_t *wr, uint8_t *new_bitmap, uint8_t *new_csums, uint32_t *modified_block) { assert(dsk->data_csum_type && dsk->csum_block_size > dsk->bitmap_granularity); assert(new_csums); uint32_t block_num = list_item(wr)->block_num; auto & inf = block_info.at(block_num); if (inf.is_writing) { return EAGAIN; } *modified_block = block_num; memcpy(wr->get_int_bitmap(this), new_bitmap, dsk->clean_entry_bitmap_size); memcpy(wr->get_checksums(this), new_csums, dsk->data_block_size/dsk->csum_block_size*(dsk->data_csum_type & 0xFF)); return 0; // !!! wr->crc32c НЕ пересчитан !!! } ``` Проверка `grep` показывает: `calc_crc32c` вызывается **только** в `add_entry:1548`. Ни `punch_holes`, ни `add_compact` (1700-1725) **не пересчитывают** crc32c существующей entry, в которой меняется содержимое. **Что произойдёт:** - При компакции/flush с включённым `perfect_csum_update` (требует `csum_block_size > bitmap_granularity`) bitmap и checksums существующего BIG_WRITE entry модифицируются. - Метаблок пишется на диск (`blockstore_flush.cpp:385`). - При следующем старте OSD `read_blocks` (`blockstore_heap.cpp:383-389`) проверяет `wr->crc32c != calc_crc32c()` → mismatch → entry classified as corrupt. - `skip_corrupted_meta_entries = false` (по умолчанию) → OSD **отказывается стартовать**. - `skip_corrupted_meta_entries = true` → BIG_WRITE **пропускается** = **полная потеря пользовательского блока данных** (данные на disk-данных есть, но ссылки в метаданных нет → расцениваются как «свободное место» и могут быть перезаписаны). **Рекомендация:** добавить `wr->crc32c = wr->calc_crc32c();` в конце `punch_holes`. Также аудитировать любые in-place изменения существующих entries. --- ### 1.3. CRITICAL — Забыт `fsync(meta_fd)` после recheck-перезаписи метаблоков в init **Файл:** `src/blockstore/blockstore_init.cpp:310-345` **Описание:** После `finish_recheck` блокстор записывает «исправленные» метаблоки (с удалённым garbage) через `io_uring_prep_writev`. Никакого `io_uring_prep_fsync(meta_fd, IORING_FSYNC_DATASYNC)` после цикла **нет**. `initialized = 10` выставляется немедленно. **Параллель:** только что закрыт аналогичный баг для compaction — коммит `4b7d0ce2 "Add data device fsyncs during compaction (forgotten during initial heap store dev)"`. Здесь тот же класс ошибки в init-пути. **Последствия:** - Если OSD отрабатывает recheck (т.е. был грязный shutdown), завершает init и **сразу теряет питание** до того, как страничный кэш ядра/write-cache диска успеют сброситься: - На следующем старте garbage entries снова обнаруживаются. - Возможна повторная double-claim ошибка (см. коммит `caa70317`), которая теперь срабатывает на разных entries и `init_erase_double_claim` начинает удалять валидные объекты. - При `disable_meta_fsync=false` (это режим **по умолчанию**) такого допускать нельзя. **Рекомендация:** добавить fsync meta после цикла `recheck_modified_blocks`, аналогично compaction-fix. --- ### 1.4. HIGH — Use-after-free в `recheck_buffer::free_entry` **Файл:** `src/blockstore/blockstore_heap.cpp:864-892` ```cpp free(li); // освобождаем modify_alloc(block_num, [&](heap_block_info_t & inf) { ... for (auto it = inf.entries.begin(); it != inf.entries.end(); it++) { if (*it == li) // !!! freed pointer comparison !!! ... ``` C++ UB. На практике glibc обычно не переиспользует адрес немедленно, но в комбинации с jemalloc/tcmalloc/thread-local arenas возможна ложная коллизия → пропуск удаления entry из `inf.entries` или падение `assert(found)`. **Рекомендация:** сохранить копию pointer для сравнения до `free(li)`, либо `free(li)` после `modify_alloc`. --- ### 1.5. HIGH — `free_buffer_area(_, 0, 0)` падает на zero-length small_write в init-пути **Файл:** `src/blockstore/blockstore_heap.cpp:712-722` (нет проверки `if (wr->small().len > 0)` в `init_free_bad_entry`, хотя такая проверка есть в аналогичных местах на строках 589 и 2001). **Последствие:** `multilist_alloc_t::do_free(0)` падает на `assert(!nexts[pos])` → крах OSD в init. OSD не может стартовать. Условие триггерится при `init_erase_double_claim` для EC-пула с zero-length small_writes (bump-версии без данных). --- ### 1.6. HIGH — Хрупкий инвариант `assert(!inf.mod_lsn_to || inf.mod_lsn_to == next_lsn)` **Файл:** `src/blockstore/blockstore_heap.cpp:1376, 1469` Если между двумя `allocate_entry` в один и тот же meta-block произошёл `allocate_entry` в **другой** block, `next_lsn` инкрементировался для чужого блока. Затем при возврате к исходному блоку `inf.mod_lsn_to == old_value != next_lsn` → ассерт. Падение OSD под обычной mixed-нагрузкой (big + small + intent). --- ### 1.7. MEDIUM — `iterate_with_stable`/`iterate_compaction` не skip garbage entries **Файл:** `src/blockstore/blockstore_heap.cpp:2030-2069, 2080-2159` В отличие от `validate_object:464` и `mark_garbage_up_to:1983`, эти итераторы не пропускают `is_garbage()` entries. Reader через `iterate_with_stable` может выбрать garbage entry с location, уже переиспользованным новой записью → **возврат wrong data клиенту**. --- ### 1.8. CRITICAL — `intent_write_allowed` без проверки `use_atomic_flag` **Файл:** `src/blockstore/blockstore_write.cpp:55-110 / 459-462` **Сценарий:** - `disable_meta_fsync=true`, `atomic_write_size=16384`, `use_atomic_flag=false`. - `intent_write_allowed` пропускает запись (`len <= atomic_write_size`), но в `resume_10` RWF_ATOMIC не ставится. - Torn write 16 КБ → испорчены данные внутри BIG_WRITE. - При `disable_meta_fsync=true` запись метаданных считается durable сразу → `trim_lsn` сжигает recheck-возможность. - Тихое необнаруживаемое повреждение данных в объекте. **Рекомендация:** в `intent_write_allowed` добавить `if (!dsk.use_atomic_flag) return false;`. --- ## 2. Критические баги в OSD primary (репликация) ### 2.1. CRITICAL — `mark_partial_write` сломан для replicated пулов **Файл:** `src/osd/osd_primary_subops.cpp:391-397` (подтверждено) **Код:** ```cpp if (opcode == OSD_OP_SEC_READ && (retval == -EIO || retval == -EDOM) || opcode == OSD_OP_SEC_WRITE && retval != expected) { ((osd_rmw_stripe_t*)subop->rmw_buf)->read_error = true; } ``` Replicated primary использует `OSD_OP_SEC_WRITE_STABLE` (см. `osd_primary_subops.cpp:204, 236`), но проверка ловит только `OSD_OP_SEC_WRITE`. Поэтому **`read_error` для replicated никогда не устанавливается**, а значит `mark_partial_write` (`osd_primary.cpp:432-453`) никаких чанков как `LOC_OUTDATED` не пометит. **Сценарий потери данных (реалистичный):** 1. Реплика-3, ENOSPC на одном из устройств. 2. WRITE: 2 реплики записали новую версию STABLE, 3-я вернула ENOSPC. 3. `read_error` для 3-й не выставлен → `mark_partial_write` не помечает 3-ю как OUTDATED. 4. Объект НЕ попадает в `misplaced_objects` / `incomplete_objects`. 5. Следующий READ выбирает реплику по `zero_read` (свой OSD или первая ненулевая). Если выбрана **отставшая** реплика → клиент тихо получает **старые данные**. 6. Следующий WRITE через RMW_READ от отстающей реплики возвращает старую `fact_ver`. `target_ver = old+1` = уже записанная новая версия на других → EEXIST → drop соединений → re-peering → возможна более тяжёлая инконсистентность. **Рекомендация:** добавить `|| opcode == OSD_OP_SEC_WRITE_STABLE` в проверку. --- ### 2.2. CRITICAL — `stripes[0]` перезаписывается для каждой реплики **Файл:** `src/osd/osd_primary_subops.cpp:167`, `src/osd/osd_primary.cpp:441-450` `submit_primary_subop_batch` для replicated использует `stripe_num = rep ? 0 : role`, т.е. `si = &stripes[0]` для всех реплик. `si->osd_num` перезаписывается на каждой итерации цикла. После завершения `stripes[0].osd_num` хранит **только последнюю** реплику. Даже если исправить #2.1, `mark_partial_write` сможет пометить максимум **одну** реплику из множества расходящихся. **Рекомендация:** для replicated завести per-replica структуру ошибок (например, массив параллельный `stripes`), а не делить общий `stripes[0]`. --- ### 2.3. CRITICAL — Отсутствует `if (n_subops > 0)` обёртка в `continue_primary_sync` для `copies_to_delete` **Файл:** `src/osd/osd_primary_sync.cpp:195-198` Коммит `5100f822` обернул аналогичные места, но **пропустил** именно этот путь: ```cpp else if (op_data->copies_to_delete) { submit_primary_del_batch(cur_op, op_data->copies_to_delete, op_data->copies_to_delete_count); resume_7: op_data->st = 7; return; ``` Если `submit_primary_del_batch` не создаст ни одного subop (рассогласование счётчиков `copies_to_delete_after_sync_count` и фактического содержимого), SYNC висит навечно. Последующие SYNC-ы блокируются друг другом → OSD выпадает из обслуживания записи. --- ### 2.4. HIGH — UB в `free(op_data->snapshot_bitmaps)` **Файл:** `src/osd/osd_primary_chain.cpp:193` `op_data->snapshot_bitmaps` указывает на офсет внутри **одной большой** `calloc_or_die`-аллокации `op_data` (`osd_primary.cpp:113-129`). `free()` на не-начале аллокации — UB; на glibc обычно `abort("invalid pointer")`. **Триггер:** chained-read для EC, когда `extend_missing_stripes` возвращает -1 (недостаточно живых OSD для восстановления). OSD аварийно завершается. --- ### 2.5. HIGH — `pg_state_bits` перезаписывается, теряется `PG_HAS_CORRUPTED` **Файл:** `src/osd/osd_primary.cpp:521-538` ```cpp if (n_corrupted > 0) { pg_state_bits |= PG_HAS_CORRUPTED; // |= } else if (n_roles < pg.pg_data_size) { pg_state_bits = PG_HAS_INCOMPLETE; // = (затирает!) } ... ``` `pg.state` не получает `PG_HAS_CORRUPTED` для первого corrupted объекта в degraded PG — оператор не видит сигнал, monitoring молчит. --- ### 2.6. HIGH — Утечка `cur_op->buf` при retry-чтения после `mark_object_corrupted` **Файл:** `src/osd/osd_primary.cpp:272, 297-303` При `goto resume_0` старый `cur_op->buf` теряется без `free()`. В `osd_primary_chain.cpp:59` это сделано корректно. Здесь — забыто. При множественных корраптингах → OOM OSD. --- ### 2.7. MEDIUM — Гонка между PG_REPEERING и in-flight WRITE **Файл:** `src/osd/osd_primary_write.cpp:225-232` PG-state проверяется один раз перед `submit_primary_subops(SUBMIT_WRITE)`. Если PG переходит в `PG_STOPPING|PG_REPEERING` **после** запуска subops, операция продолжается. При быстрой смене primary возможна ситуация двух «пишущих» primary одновременно (split-brain в окне re-peering). --- ### 2.8. CRITICAL — Объект OBJ_CORRUPTED-only → крах OSD при write/delete **Файл:** `src/osd/osd_peering_pg.cpp:387-402` + `osd_primary.cpp:676-678` Объект со state `OBJ_CORRUPTED` (без других флагов) попадает в `misplaced_objects` через else-ветку. `remove_object_from_state` использует цепочку `else if` для INCONSISTENT/INCOMPLETE/DEGRADED/MISPLACED, а для CORRUPTED-only-state выкидывает `throw runtime_error("BUG: Invalid object state: ...")`. **Триггер:** - EC-пул, объект имеет полный набор чистых чанков на cur_set + extra-чанк, оставшийся от rebalance. - Extra-чанк corrupts (сбой носителя). - Scrub помечает extra-чанк → `add_object_to_set` даёт `obj_state = OBJ_CORRUPTED`-only. - Объект сохранён в `misplaced_objects`. - Следующая обычная write/delete операция вызывает `remove_object_from_state` → throw → **крах OSD**. - При перезапуске сценарий повторяется → бесконечный crash-loop. --- ## 3. Критические баги в EC / RMW ### 3.1. CRITICAL — `ec_check_combination` модифицирует оригинальные `bmp_buf` стрипов **Файл:** `src/osd/osd_rmw.cpp:1198-1216` (внутри `ec_find_good`) Функция использует `bitmap_set` непосредственно на оригинальных `stripes[i].bmp_buf`. После возврата вызывающий получает повреждённые bitmaps. При scrub с `bitmap_size > 0` возможна потеря информации о sparse regions. --- ### 3.2. CRITICAL — `ec_find_good` не ловит ambiguity при «меньшем найденном первым» **Файл:** `src/osd/osd_rmw.cpp:1361-1378` ```cpp if (found_valid.size() >= valid_chunks.size() && found_valid != valid_chunks) // ambiguity ``` Этот код ловит ambiguity ТОЛЬКО когда новый valid set ≤ предыдущего. Если первая комбинация дала меньший valid set, а вторая — больший, ambiguity молчаливо игнорируется И больший набор отбрасывается. При `scrub_find_best = true` (default) → scrub может принять неверный набор данных как «good» и репарировать остальные на его основе. --- ### 3.3. CRITICAL — Majority voting репарирует «правильную» копию из 2 испорченных **Файл:** `src/osd/osd_scrub.cpp:493-538` + документация на `scrub_find_best` Если 3 реплики и 2 одинаково испорчены (тот же дефект на 2 HDD с одинаковой прошивкой) — majority выбирает «плохую» версию как эталон, помечает хорошую копию как corrupted, и recovery затем перезаписывает хорошую копию плохими данными. Все 3 реплики становятся битыми. Документация (`docs/config/osd.en.md`) явно признаёт сценарий, но `scrub_find_best=true` остаётся **default**. Для коммерческого облака рекомендуется выставлять `scrub_find_best=false` глобально. --- ### 3.4. CRITICAL — OOB-доступ `op_data->stripes[chunk.role]` для EC с OUTDATED-чанками **Файл:** `src/osd/osd_scrub.cpp:630` Массив `stripes` имеет размер `stripe_count` (= число не-OUTDATED чанков), а `chunk.role` может быть `[0, pg_size)`. Для EC объектов с OUTDATED-чанками возникает OOB-чтение → ложное снятие/установка `LOC_CORRUPTED` на основе случайных байт. --- ### 3.5. HIGH — `reconstruct_stripes_ec` без assert на корректное число чанков **Файл:** `src/osd/osd_rmw.cpp:410-419` Если функция вызвана при количестве доступных чанков < `pg_minsize` (логическая ошибка выше по стеку), цикл сбора `data_ptrs` не заполнит все позиции, и ISA-L использует неинициализированные указатели → чтение мусора → реконструкция из мусора → **записанные мусорные данные** при recovery. --- ### 3.6. HIGH — jerasure unaligned branch расширяет цикл за `pg_minsize` **Файл:** `src/osd/osd_rmw.cpp:556-565` `for (role = 0; role < pg_size; role++)` отличается от aligned-ветки (`role < pg_minsize`). При missing parity с `read_end != 0` используется `decoding_matrix + (role*pg_minsize)` за пределами выделенного массива → потенциальный OOB read и неверная реконструкция. --- ## 4. Критические баги в client / write-back cache ### 4.1. CRITICAL — Underflow `writeback_bytes` при flush последовательных DELETE **Файл:** `src/client/cluster_client_wb.cpp:226, 447` (подтверждено) ```cpp writeback_bytes += is_del ? 0 : op->len; // строка 226: DELETE не добавляет ... writeback_bytes -= off - from_it->first.stripe; // строка 447: вычитает полностью assert(writeback_queue_size > 0 || !writeback_bytes); ``` При series соседних `OSD_OP_DELETE` (которые группируются в одну batch) `writeback_bytes` уходит в underflow `uint64_t`. В debug-сборке падает assert → потеря всех unflushed данных. В release (assert disabled) `while (wb->writeback_bytes > client_max_buffered_bytes ...)` в `execute_internal:784` превращается в **бесконечный цикл** на следующей операции записи. --- ### 4.2. CRITICAL — Heap overflow в `copy_write` при некорректном iov **Файл:** `src/client/cluster_client_wb.cpp:247-258` ```cpp while (iov_idx < op->iov.count) { auto & iov = op->iov.buf[iov_idx]; assert(pos + iov.iov_len <= len); memcpy(buf + pos, iov.iov_base, iov.iov_len); ... } ``` `buf` выделен размером `op->len`. Если caller передал `op->iov` с суммарной длиной > `op->len`, в release-сборке (assert исчез) `memcpy` пишет за границей буфера → heap corruption. --- ### 4.3. CRITICAL — Фантомный read данных несостоявшегося write **Файл:** `src/client/cluster_client.cpp:1174-1180` При fatal-error пути (`-EROFS`, `-EACCES`, `-EINVAL` и т.п.) `mark_flush_written` НЕ вызывается, и CACHE_REPEATING-буфер остаётся в `dirty_buffers` навсегда. Последующий **read** того же региона в той же сессии копирует данные из этого буфера через `read_from_cache:515`. Application получает данные write'а, который **вернул ошибку**. Это нарушает базовую семантику и в коммерческом облаке потенциально приводит к скрытому повреждению на уровне приложения клиента (например, файловые системы поверх RBD). --- ### 4.4. HIGH/CRITICAL — SYNC возвращает успех при disconnected OSD **Файл:** `src/client/cluster_client.cpp:1467-1483` При disconnect OSD между write OK и client SYNC: 1. Buffer становится CACHE_REPEATING (через `repeat_ops_for` после disconnect). 2. SYNC удаляет OSD из `dirty_osds` (он не в `msgr.osd_peers`). 3. `dirty_osds` становится пустым → **SYNC возвращает 0 мгновенно**. 4. Application считает данные на диске, отпускает их. 5. Если клиент крашится до завершения flush_buffers re-send — данные потеряны. Нарушение семантики fsync. --- ### 4.5. HIGH — TOCTOU в `send_sync` **Файл:** `src/client/cluster_client.cpp:1536` `msgr.osd_peers.at(part->osd_num)` бросает исключение, если в callback'ах внутри цикла отправки произошёл `stop_client`. → необработанный exception → крах клиента с возможным неконсистентным writeback. --- ### 4.6. HIGH — Reentrant `stop_client` в `handle_op_part` **Файл:** `src/client/cluster_client.cpp:1611-1614` Авторами помечено FIXME (`// FIXME postpone such things to set_immediate here to avoid bugs`). При нескольких parts на одного primary с -EPIPE возможен двойной вызов `continue_sync/rw` на одну op и нарушение инварианта `inflight_count`. --- ### 4.7. HIGH — `bitmap_buf` не обнуляется при reslice **Файл:** `src/client/cluster_client.cpp:1263-1269` Обнуление выполняется только если `bitmap_mem > bitmap_buf_size`. При reslice без расширения область `part_bitmaps` хранит биты прошлого slice'а. В сочетании с server'ом, который возвращает короткий bitmap, → ложные «1» в bitmap для snapshot/chain reads → пропуск обращения к parent inode → **incorrect read**. --- ## 5. Критические баги в scrub ### 5.1. CRITICAL — Replicated scrub помечает только канонического члена минорной группы **Файл:** `src/osd/osd_scrub.cpp:488-522` Цикл `votes[eq_to[role]]` заполняет только канонические индексы каждого класса эквивалентности; цикл маркировки итерирует по `role` и видит `votes[role]==0` для не-канонических → они **не помечаются как LOC_CORRUPTED**. **Эффект:** в группе из 2 «плохих» копий с одинаковыми данными после scrub'а одна (каноническая) помечена, вторая — нет. При следующем read primary может выбрать неотмеченную плохую копию (по `zero_read`) → клиент получает битые данные. При recovery `read_target` включает плохую копию → распространение порчи. --- ### 5.2. CRITICAL — OOB stripes[chunk.role] при OUTDATED-чанках См. §3.4. --- ### 5.3. CRITICAL — Majority voting с `scrub_find_best=true` См. §3.3. --- ### 5.4. HIGH — Scrub отключён для clean-объектов в degraded PG **Файл:** `src/osd/osd_scrub.cpp:384-393, 426` Для clean-объекта stripes создаются из `cur_set`, в котором может быть `osd_num=0` (отключённый OSD). `submit_primary_subop` для osd_num=0 немедленно возвращает -EPIPE. `errcode != EIO/EDOM` → `continue_primary_scrub` (строки 663-665) делает `finish_op` без `scrub_check_results`. **Scrub фактически не работает** ни для одного clean-объекта в любом PG с хоть одним выпавшим OSD. В реальном кластере это окно может длиться часами/днями (recovery). Silent corruption за это время не выявляется. --- ### 5.5. HIGH — `get_object_osd_set` игнорирует `PG_HAS_CORRUPTED/INCONSISTENT` **Файл:** `src/osd/osd_primary.cpp:184-211` Не проверяет `PG_HAS_INCONSISTENT/CORRUPTED` — если PG содержит только corrupted/inconsistent объекты (без degraded/misplaced/incomplete), возвращается `cur_set` (включая corrupted-OSD), а не `read_target` → re-scrub читает с corrupted-источника. --- ### 5.6. LOW — Hang scrub при stripe_count == 0 **Файл:** `src/osd/osd_scrub.cpp:426-433` Если все чанки объекта помечены LOC_OUTDATED, `n_subops=0`, но `op_data->st = 1` уже выставлен. Никто не разбудит. Слот в `scrub_queue_depth` блокируется навсегда. --- ## 6. Критические баги в peering/cluster ### 6.1. CRITICAL — OBJ_CORRUPTED-only объект → крах OSD См. §2.8. --- ### 6.2. HIGH — Memory leak в `remove_object_from_state` recheck path **Файл:** `src/osd/osd_primary.cpp:613-622` При `recheck_state == NULL` и `(*object_state)->ref_count` после декремента == 0 entry в `state_dict` не очищается. --- ### 6.3. MEDIUM — `drop_dirty_pg_connections` не вызывается в else-ветке `repeer_pg` **Файл:** `src/osd/osd_peering.cpp:175-193` При переходе PG_ACTIVE → PG_REPEERING без `start_pg_peering`, dirty_pg_connections не сбрасываются. Клиенты с unsynced writes получают задержки/таймауты. --- ## 7. Критические баги в monitor / etcd ### 7.1. CRITICAL — `save_last_clean()` и `update_total_stats()` без проверки LEASE **Файл:** `mon/mon.js:304, 802` В отличие от `save_pg_config():681`, эти транзакции не проверяют `LEASE` на `/mon/master`. Старый монитор после потери lease может перезаписать `last_clean_pgs` устаревшими данными, что повредит логику `generate_pool_pgs` нового мастера → лишние data movements, потенциально incorrect placement. --- ### 7.2. CRITICAL — `antietcd_adapter.become_master()` без CAS **Файл:** `mon/antietcd_adapter.js:126-148` В отличие от `etcd_adapter.become_master`, проверка `CREATE 0` отсутствует. Безусловная запись `/mon/master`. При кратковременном split-brain в antietcd raft два «лидера» могут оба перезаписать ключ. --- ### 7.3. HIGH — `schedule_next_recheck_at()`: таймер срабатывает мгновенно **Файл:** `mon/mon.js:722` ```js setTimeout(() => { ... }, now - this.next_recheck_at); ``` `now - next_recheck_at` отрицательно/нулевое (next в будущем) → таймер срабатывает сразу, ещё и без `* 1000`. Политика `osd_out_time` (выкидывание OSD из дерева через 600 сек) фактически не работает корректно. PG висят в degraded дольше → при следующей потере OSD возможен переход в incomplete. --- ### 7.4. HIGH — Use-after-free в etcd retry-таймерах **Файл:** `src/client/etcd_state_client.cpp:151-155` и аналогичные в `osd_cluster.cpp` Лямбды захватывают `this`. Деструктор `etcd_state_client_t` не отменяет произвольные retry-таймеры → при разрушении объекта (NFS proxy, dynamic reconfiguration) таймер триггерится с уже удалённым `this`. --- ### 7.5. HIGH — `etcd_watch_revision_*` устаревает при `etcd_watches_initialised < ETCD_TOTAL_WATCHES` **Файл:** `src/client/etcd_state_client.cpp:418-442` События watch обрабатываются сразу, но `etcd_watch_revision_*` обновляется только после полной инициализации всех watches. При обрыве/reconnect возможны дублирующие обработки событий, в частности пропуск `on_change_pg_state_hook` для зависших операций. --- ### 7.6. HIGH — `clean_nonexistent_pgs` не убирает удалённые inode'ы **Файл:** `src/client/etcd_state_client.cpp:736 / 770-822` При reload через compaction inode_config не очищается. Клиент пишет в удалённый inode → -EPIPE/-ENOENT. Не data corruption, но liveness-проблема. --- ### 7.7. MEDIUM — `reshard_continue()` UB на пустом векторе **Файл:** `src/osd/osd_cluster.cpp:803-810` `reshard_pools[0]` без проверки `.empty()` после goto-цикла. --- ## 8. Критические баги в messenger (TCP / RDMA) ### 8.1. HIGH — Утечка `zc_free_list` для партий без `MSGR_SENDP_FREE` **Файл:** `src/client/msgr_send.cpp:295-340` Если партия (типичная для клиента) не содержит FREE-операций, NULL-маркер всё равно добавляется. Второе уведомление (`prev=true`) удаляет операции только при `i > 0` — NULL-маркер не убирается, и последующие операции с FREE «застревают» за чужими NULL-маркерами. Долгоживущее соединение OSD → OOM → крах OSD. --- ### 8.2. MEDIUM — Отсутствие валидации длин в `handle_op_hdr` **Файл:** `src/client/msgr_receive.cpp:264-346` `sec_rw.len`, `attr_len`, `sec_stab.len`, `json_len` принимаются без проверки и сразу передаются в `malloc_or_die`. При `UINT32_MAX` процесс падает через `exit(1)`. DoS, но не data corruption. В trusted cluster не критично; при компрометации хотя бы одного OSD-узла — критично. --- ### 8.3. MEDIUM — `bitmap_len` короче ожидаемого → mixed-state в `op->bitmap` **Файл:** `src/client/msgr_receive.cpp:366-380` Если OSD возвращает bitmap короче `op->bitmap_len`, в `op->bitmap` остаются неинициализированные/устаревшие байты. При EC-recovery это приведёт к неверной интерпретации stripes → клиент получает corrupted data. Требует ошибки сервера, защиты в клиенте нет. --- ### 8.4. LOW — Ping-FIXME при RDMA-transition **Файл:** `src/client/msgr_receive.cpp:78-81` (`FIXME: Ignore pings during RDMA state transition`). PING-reply пропускается → keepalive таймаут → ложный дисконнект и retry. --- ## 9. Критические баги в disk_tool ### 9.1. CRITICAL — `raw_resize()` всегда возвращает 0 **Файл:** `src/disk_tool/disk_tool_resize.cpp:114` (подтверждено) ```cpp ret: free_new_meta(); return 0; // ВСЕ ошибки маскируются ``` Вызывающий код (`disk_tool_resize_auto.cpp:111`, `disk_tool_upgrade.cpp:134`) при «успехе» начинает: 1. Очищать старые суперблоки (`clear_osd_superblock`). 2. Записывать новые суперблоки. При реальной ошибке resize данные могут быть частично перемещены / журнал и метаданные побиты, **но старый суперблок уже стёрт**. OSD не сможет стартовать; восстановление требует bare-metal вмешательства. **Рекомендация (1 символ):** `return r;`. --- ### 9.2. CRITICAL — `write_osd_superblock` / `clear_osd_superblock` / `write_zero` без fsync **Файл:** `src/disk_tool/disk_tool_udev.cpp:141, 423-427`, `src/disk_tool/disk_tool_utils.cpp:278-300` После записи суперблока просто `close(fd)` без `fsync`. При write-back кэше диска (норма!) и потере питания: - `prepare`: суперблок «записан», но не на пластинах → OSD не стартует после reboot. - `resize_data`: новые суперблоки могут пропасть, но физическое перемещение данных уже произошло. - `purge`: суперблоки «очищены», но кэш может их вернуть — старый OSD может попытаться зарегистрироваться обратно. --- ### 9.3. CRITICAL — Не сбрасывается состояние KV-БД при close/open **Файл:** `src/kv/kv_db.cpp:680-699, 506-546` `close()` сбрасывает только `block_cache.clear()` и `known_versions.clear()`. Остаются `block_levels`, `allocating_blocks`, `new_versions` и т.д. При reopen на другой inode_id `confirmed=true` allocating blocks содержат смещения от предыдущего inode → новые записи в новый inode перепишут существующие KV-блоки на старте. --- ### 9.4. HIGH — Опечатка `allocating_block_pos` вместо `next` в `alloc_block` **Файл:** `src/kv/kv_db.cpp:712` ```cpp if (allocating_blocks[allocating_block_pos].offset != UINT64_MAX && ... ) ``` Должно быть `allocating_blocks[next].offset`. Лишние allocating blocks создаются, фрагментация растёт. --- ### 9.5. HIGH — Double-close при общем fd для journal и meta **Файл:** `src/disk_tool/disk_tool_meta.cpp:229-239` После `close(meta_fd); meta_fd = -1;` проверка `journal_fd != meta_fd` всегда true → double-close. В однопоточной утилите безвредно (EBADF), но при добавлении многопоточности → race с чужим fd. --- ### 9.6. HIGH — Орфанные KV-блоки при split-сбое root **Файл:** `src/kv/kv_db.cpp:1856-1922` `clear_block` использует `version=0`, что в CAS возвращает -EEXIST для уже-аллоцированных inode_block. Орфанные блоки никогда не очищаются → утечка пространства KV-индекса. --- ## 10. Util / Misc ### 10.1. HIGH — SQE128 partial init в ringloop **Файл:** `src/util/ringloop.cpp:93, 184-200` `*sqe = { 0 };` обнуляет 64 байта, реальный SQE128 — 128 байт. `restore()` и `space_left()` используют `>> sqe_shift`, но `sqe_tail` инкрементируется на 1. Сейчас активируется только в ublk_server control path → не corruption, но мина при будущем использовании SQE128 в data path. ### 10.2. MEDIUM — `readv_blocking` infinite loop при EOF **Файл:** `src/util/rw_blocking.cpp:60-93` `r == 0` от readv не обрабатывается → бесконечный цикл. Используется в `fio_sec_osd.cpp` (тестовая утилита), не в OSD. ### 10.3. LOW — `free()` C++ объектов в `~ring_loop_t()` Не вызывает деструкторы `std::function` callbacks → потенциальный memory leak. --- ## 11. Что НЕ найдено / выглядит корректно Аудит подтвердил корректность: - Порядок fsync для BIG_WRITE: data → fsync → meta. - Recheck SMALL_WRITE/INTENT_WRITE по CRC32C при инициализации. - `submit_fsyncs` выбор FD. - `completed_lsn` в суперблоке через `RWF_DSYNC`. - `mark_completed_lsns` для out-of-order meta-блоков. - Защита PG locks через `sec_check_pg_lock` + lease. - Stabilize/rollback ordering per-OSD через TCP serialization. - Сериализация per-oid write через `write_queue` (для не-scrub операций). Эти места выглядят надёжно реализованными. --- ## 12. Приоритизированный список действий ### Срочно исправить перед запуском в продакшен 1. **`src/util/crc32c.c:413`** — заменить `if (left_pad > 0)` на `if (right_pad > 0)`. 2. **`src/blockstore/blockstore_heap.cpp:1727-1742`** — добавить `wr->crc32c = wr->calc_crc32c();` в конце `punch_holes`. Дополнительно: проверить, что аналогичная пересчёт делается в `add_overwrite_in_place` и других in-place модификаторах. 3. **`src/blockstore/blockstore_init.cpp:310-352`** — добавить `io_uring_prep_fsync(meta_fd, IORING_FSYNC_DATASYNC)` после цикла перезаписи `recheck_modified_blocks` (если `!disable_meta_fsync && !readonly`). 4. **`src/osd/osd_primary_subops.cpp:391-397`** — добавить `OSD_OP_SEC_WRITE_STABLE` в проверку `read_error`. 5. **`src/osd/osd_primary_subops.cpp:167` + `osd_primary.cpp:441-450`** — переработать `mark_partial_write` для replicated: хранить per-replica статус ошибки отдельно, а не shared `stripes[0]`. 6. **`src/osd/osd_primary_sync.cpp:195-198`** — обернуть путь `copies_to_delete` в `if (n_subops > 0)`. 7. **`src/osd/osd_primary_chain.cpp:193`** — убрать `free(op_data->snapshot_bitmaps)` (память освобождается с `op_data`). 8. **`src/osd/osd_peering_pg.cpp:387-402`** — добавить ветку для `OBJ_CORRUPTED`-only состояния, либо обработать его в `remove_object_from_state`. 9. **`src/osd/osd_primary.cpp:521-538`** — заменить `=` на `|=` для всех веток `pg_state_bits` (теряется `PG_HAS_CORRUPTED`). 10. **`src/osd/osd_scrub.cpp:488-522`** — итерировать маркировку по `eq_to[role]`-классу, помечая все копии в классе. 11. **`src/osd/osd_scrub.cpp:630`** — индексировать массив `stripes` по позиции в нём, а не по `chunk.role` (или через map `role → stripe index`). 12. **`src/osd/osd_scrub.cpp:384-393`** — пропускать `osd_num=0` чанки в submit_scrub_subops для clean-объектов (либо помечать их `not_exists=true` без отправки). 13. **`src/disk_tool/disk_tool_resize.cpp:114`** — заменить `return 0;` на `return r;`. 14. **`src/disk_tool/disk_tool_udev.cpp:141, 423`** + `disk_tool_utils.cpp:278` — добавить `fsync(fd)` перед `close(fd)` для всех записей суперблока и `write_zero`. 15. **`src/client/cluster_client_wb.cpp:447`** — `writeback_bytes -=` должно учитывать только не-DELETE буферы; либо `+=` на 226 должен `+= op->len` независимо от is_del. 16. **`src/client/cluster_client.cpp:1174-1180`** — при fatal-error вызывать `wb->mark_flush_written(op->inode, op->offset, op->len, op->flush_id)` до `erase_op`. 17. **`src/client/cluster_client.cpp:1475-1483`** — при удалении OSD из `dirty_osds` проверять наличие CACHE_REPEATING-буферов на тот же OSD; не возвращать SYNC OK, пока re-flush не завершится. 18. **`mon/mon.js:304, 802`** — добавить `compare: [{ target:'LEASE', lease: etcd_lease_id, key: ... '/mon/master' }]` в обе транзакции. 19. **`mon/antietcd_adapter.js:141`** — добавить `compare: [{ target:'CREATE', create_revision:0, key:'/mon/master' }]`. 20. **`mon/mon.js:722`** — заменить `setTimeout(cb, now - next)` на `setTimeout(cb, (next - now) * 1000)`. ### Важно — в ближайшем релизе 21. `intent_write_allowed` (`blockstore_write.cpp:55-110`) — добавить `if (!dsk.use_atomic_flag) return false;`. 22. `iterate_with_stable` / `iterate_compaction` — пропускать `is_garbage()` entries. 23. `recheck_buffer::free_entry` — переставить порядок `free(li)` и `modify_alloc`. 24. `init_free_bad_entry` — проверка `if (wr->small().len > 0)` перед `free_buffer_area`. 25. `osd_primary.cpp:303` — `free(cur_op->buf); cur_op->buf = NULL;` перед `goto resume_0`. 26. Default `scrub_find_best = false` для коммерческого окружения (см. §3.3). 27. SQE128 ringloop — синхронизировать инкременты sqe_tail / space_left с реальной семантикой. 28. cluster_client deструктор — auto-flush для безопасности. ### Защита-в-глубину 29. Защита от reentrant `stop_client` в `handle_op_part` (FIXME уже стоит). 30. Валидация длин в `handle_op_hdr` — отвергать с -EINVAL вместо `exit(1)`. 31. Per-FD retry-таймеры в etcd_state_client с регистрацией для отмены в деструкторе. 32. Обработка `OBJ_CORRUPTED` в `get_object_osd_set` (включить в проверку маски). --- ## 13. Замечания об устройстве кода и о методе аудита 1. Большинство «строгих» asserts в blockstore (`assert(res==0)`, `assert(li_entry_type == cwr_entry_type)` и т.п.) дают защиту fail-stop, но иногда срабатывают на сценариях, которые **могли бы** возникнуть, если бы вышестоящий слой OSD когда-нибудь нарушил инвариант. В долгоживущем продакшене такие assert'ы — мины: их активация = полный downtime OSD без возможности recovery. 2. Recent commit history (`4b7d0ce2`, `2a6ce356`, `f6e07f57`, `04fe7eb2`, `21f712ad`, `f271c845`, `fe8b1fe0`, `1ec963e4`, `5100f822`, `87a52307`, `0c5e6d43`, `b2780874`, `caa70317`) — большой поток bugfix'ов конкретно в новом heap-блокстoре и в путях fsync/durability. Это **сигнал**: код **только что** стабилизируется. Для коммерческого облака с гарантиями данных рекомендуется: - Развёрнуть текущую версию **только** на стенде, прогнать её под продакшен-нагрузкой и сбойными сценариями (kill -9 OSD, отключение питания узла, потеря NIC, чтения во время компакции, scrub во время writeы) минимум 4-6 недель. - Параллельно отправить найденные находки разработчику для исправления. - Не запускать на heap-blockstore с `perfect_csum_update=true` до исправления #1.2. 3. **scrub_find_best=true (default)** — для публичного облака необходимо переключить на `false`, чтобы исключить риск «починки» хорошей копии 2 одинаково-битыми (см. §3.3). 4. Vitastor требует **доверенной cluster network**: messenger не имеет аутентификации/HMAC. Сетевая безопасность должна обеспечиваться отдельно (изоляция VLAN, eBPF firewall). --- ## 14. Приложение: что было проверено Аудит охватил полное чтение и анализ следующих файлов (всего ≈25.5 KLOC C++ кода + JS-монитор): - `src/blockstore/`: `blockstore.cpp/h`, `blockstore_disk.cpp/h`, `blockstore_flush.cpp/h`, `blockstore_heap.cpp/h`, `blockstore_impl.cpp/h`, `blockstore_init.cpp/h`, `blockstore_open.cpp`, `blockstore_read.cpp`, `blockstore_stable.cpp`, `blockstore_sync.cpp`, `blockstore_write.cpp`, `multilist.cpp/h`, `ondisk_formats.h` - `src/osd/`: `osd.cpp/h`, `osd_cluster.cpp`, `osd_flush.cpp`, `osd_peering.cpp`, `osd_peering_pg.cpp/h`, `osd_primary.cpp/h`, `osd_primary_chain.cpp`, `osd_primary_describe.cpp`, `osd_primary_subops.cpp`, `osd_primary_sync.cpp`, `osd_primary_write.cpp`, `osd_rmw.cpp/h`, `osd_rmw_test.cpp`, `osd_scrub.cpp`, `osd_secondary.cpp` - `src/client/`: `cluster_client.cpp/h`, `cluster_client_impl.h`, `cluster_client_list.cpp`, `cluster_client_wb.cpp`, `etcd_state_client.cpp/h`, `http_client.cpp/h`, `messenger.cpp/h`, `msgr_op.cpp/h`, `msgr_receive.cpp`, `msgr_send.cpp`, `msgr_stop.cpp`, `msgr_rdma.cpp/h`, `msgr_rdmacm.cpp`, `osd_ops.cpp/h`, `pg_states.cpp/h` - `src/util/`: crc32c, ringloop, timerfd_manager, allocator, rw_blocking, epoll_manager, sha256, addr_util - `src/disk_tool/`: prepare, resize, resize_auto, journal, meta, udev, upgrade, utils, discard, simple_offsets - `src/kv/`: kv_db - `mon/`: mon.js, antietcd_adapter.js, etcd_adapter.js, pg_gen.js, pg_utils.js Подтверждены прямым повторным чтением: - `crc32c_pad` left_pad/right_pad баг (CRITICAL #1) - `punch_holes` без crc32c (CRITICAL #2) - `mark_partial_write` для SEC_WRITE_STABLE (CRITICAL #3) - `raw_resize` всегда 0 (CRITICAL #5) - `writeback_bytes` underflow для DELETE (CRITICAL #6) Все приведённые номера строк актуальны для коммита `4b7d0ce2`.
Owner

Норм, продолжаем дебаг )))

  1. исправил, но забавно, что опять ни на что не влияло - crc32c_pad использовался в старом сторе и там паддинги всегда кратны 4096, поэтому в ветку right_pad > 0 оно никогда не заходит по факту. проявилось бы только если кто-то блок не кратный 4096 поставил.
  2. точно, исправил, добавил тест. хотя конфигурация не дефолтная, про эту опцию гарантированно никто из юзеров вообще не знает :))). я подумывал этот хак вообще выпилить.
  3. тут на самом деле реальный баг это 3+4 и формулируется как "mark_partial_write некорректно работает для реплик т.к. ему негде учитывать read_error, т.к. stripe всего 1". зато рядом заметил похожую ошибку где был пропущен SEC_WRITE_STABLE в учёте статистики записи по операциям локального блокстора. т.е. вероятно что-то в статистику не попадало. это поправил.
  4. пока не исправлял.
  5. поправил код возврата.
  6. хрен его знает на самом деле зачем я вообще делал эту writeback буферизацию DELETE-ов. на будущее видимо. т.к. сейчас единственная команда которая в принципе отправляет операции удаления - это vitastor-cli rm/rm-data. но там writeback не включается, cli не ставит флаг client_writeback_allowed. а если его поставить принудительно (это режим "делай что говорю"), удаление перестаёт работать, т.к. оно передаёт длину 0 и ничего не буферизуется. но это ставит более широкий вопрос - в клиенте DELETE это именно удаление объекта, в который попало переданное смещение, а не вырезание куска объекта. а в writeback подразумевается именно вырезание куска объекта. то есть смешанная семантика и по сути на данный момент не используемая фича. так что проверку is_del добавил чисто для порядка и добавил FIXME насчёт буферизации удалений.
  7. да хз, зачем ему очищаться. вообще FLUSH_BUFFERS ретраится всегда, там не бывает фатальных ошибок. а если ошибка вдруг дойдёт до колбэка FLUSH_BUFFERS, клиент просто по ассерту упадёт и всё.
  8. мне кажется это неправда, хотя можно и ещё проверить, но на подобное вроде даже есть тест в test_cluster_client. там механика такая что при repeat_ops в НАЧАЛО очереди добавятся повторы буферов и синки будут сначала ждать их.
  9. вот это прикольно. но кейс конечно очень узкий - это нужно 5 реплик, 3 правильных и 2 неправильных. и тогда типа 1 из 2 не пометится неправильной. ну ладно, поправил.
  10. тут надо разобраться как правильно пофиксить эти скрабы с OUTDATED, но баг вроде действительный.
  11. а тут нет, не бывает объектов в стейте только OBJ_CORRUPTED. оно всегда либо с MISPLACED, либо с DEGRADED, либо с INCOMPLETE, либо с INCONSISTENT.
  12. во, это да, точно. fsync там был но не в том месте. переставил его в конец инициализации, после перезаписи recheck_modified_blocks.
  13. в общем косметика, типа ну перезапишет старый монитор статистику, ну и хрен с ней, на следующем проходе пересчитается. но можно добавить проверку, в принципе хуже не станет.
Норм, продолжаем дебаг ))) 1. исправил, но забавно, что опять ни на что не влияло - crc32c_pad использовался в старом сторе и там паддинги всегда кратны 4096, поэтому в ветку right_pad > 0 оно никогда не заходит по факту. проявилось бы только если кто-то блок не кратный 4096 поставил. 2. точно, исправил, добавил тест. хотя конфигурация не дефолтная, про эту опцию гарантированно никто из юзеров вообще не знает :))). я подумывал этот хак вообще выпилить. 3. тут на самом деле реальный баг это 3+4 и формулируется как "mark_partial_write некорректно работает для реплик т.к. ему негде учитывать read_error, т.к. stripe всего 1". зато рядом заметил похожую ошибку где был пропущен SEC_WRITE_STABLE в учёте статистики записи по операциям локального блокстора. т.е. вероятно что-то в статистику не попадало. это поправил. 4. пока не исправлял. 5. поправил код возврата. 6. хрен его знает на самом деле зачем я вообще делал эту writeback буферизацию DELETE-ов. на будущее видимо. т.к. сейчас единственная команда которая в принципе отправляет операции удаления - это vitastor-cli rm/rm-data. но там writeback не включается, cli не ставит флаг client_writeback_allowed. а если его поставить принудительно (это режим "делай что говорю"), удаление перестаёт работать, т.к. оно передаёт длину 0 и ничего не буферизуется. но это ставит более широкий вопрос - в клиенте DELETE это именно удаление объекта, в который попало переданное смещение, а не вырезание куска объекта. а в writeback подразумевается именно вырезание куска объекта. то есть смешанная семантика и по сути на данный момент не используемая фича. так что проверку is_del добавил чисто для порядка и добавил FIXME насчёт буферизации удалений. 7. да хз, зачем ему очищаться. вообще FLUSH_BUFFERS ретраится всегда, там не бывает фатальных ошибок. а если ошибка вдруг дойдёт до колбэка FLUSH_BUFFERS, клиент просто по ассерту упадёт и всё. 8. мне кажется это неправда, хотя можно и ещё проверить, но на подобное вроде даже есть тест в test_cluster_client. там механика такая что при repeat_ops в НАЧАЛО очереди добавятся повторы буферов и синки будут сначала ждать их. 9. вот это прикольно. но кейс конечно очень узкий - это нужно 5 реплик, 3 правильных и 2 неправильных. и тогда типа 1 из 2 не пометится неправильной. ну ладно, поправил. 10. тут надо разобраться как правильно пофиксить эти скрабы с OUTDATED, но баг вроде действительный. 11. а тут нет, не бывает объектов в стейте только OBJ_CORRUPTED. оно всегда либо с MISPLACED, либо с DEGRADED, либо с INCOMPLETE, либо с INCONSISTENT. 12. во, это да, точно. fsync там был но не в том месте. переставил его в конец инициализации, после перезаписи recheck_modified_blocks. 13. в общем косметика, типа ну перезапишет старый монитор статистику, ну и хрен с ней, на следующем проходе пересчитается. но можно добавить проверку, в принципе хуже не станет.
Author

Спасибо за быстрые фиксы. Проверил все 13 коммитов между 4b7d0ce2..6d7a7ae8 — все корректны. Прогнали ещё один раунд на коммите 6d7a7ae8, нашли новые места. Ниже только то, что ещё не обсуждали.

⚠️ Возражение по поводу OBJ_CORRUPTED-only (#11 в прошлом списке)

Ты ответил: «не бывает объектов в стейте только OBJ_CORRUPTED, оно всегда с MISPLACED/DEGRADED/INCOMPLETE/INCONSISTENT».

Перепроверили — это утверждение неверно для EC. В osd_primary.cpp:514-544:

if (n_corrupted > 0) {
    obj_state |= OBJ_CORRUPTED;
    pg_state_bits |= PG_HAS_CORRUPTED;
}
if (n_invalid > 0 || n_inconsistent > 0)  { obj_state |= OBJ_INCONSISTENT; }
else if (n_roles < pg.pg_data_size)       { obj_state |= OBJ_INCOMPLETE; }
else if (n_roles < pg.pg_cursize)         { obj_state |= OBJ_DEGRADED; }
else if (n_misplaced > 0 || n_outdated > 0) { obj_state |= OBJ_MISPLACED; }

Сценарий: EC pg_size=3, pg_data_size=2. Объект имеет все 3 chunks на cur_set. Scrub помечает parity (role=2) как corrupted:

  • n_corrupted = 1
  • n_roles = 2 (роли 0 и 1 целые) — НЕ меньше pg_data_size (2)
  • n_misplaced = 0 (corrupted чанки не считаются как misplaced)
  • n_outdated = 0, n_inconsistent = 0, n_invalid = 0

→ obj_state == OBJ_CORRUPTED (только этот флаг).

Затем remove_object_from_state (osd_primary.cpp:631-685) проходит corrupted_objects-- в первом блоке, потом else if (INCONSISTENT/INCOMPLETE/DEGRADED/MISPLACED) все false → финальный else throw runtime_error("BUG: Invalid object state: ...") → краш OSD при следующем write/delete на этот объект.

Воспроизводимо в EC-пуле при partial corruption на parity-чанке.


Новые критические находки (HEAD 6d7a7ae8)

1. CRITICAL — UBLK: инвертированный UBLK_ATTR_VOLATILE_CACHE → потеря данных при IMMEDIATE_NONE

Файл: src/client/ublk_server.cpp:285, 318

const bool writeback = cli->get_immediate_commit(inode);
...
add_device(req_dev_num,
    (writeback ? UBLK_ATTR_VOLATILE_CACHE : 0) | ...);

get_immediate_commit() возвращает true когда писания немедленно персистентны (нет кеша). UBLK_ATTR_VOLATILE_CACHE должен значить есть волатильный кеш (требует FLUSH). Семантика инвертирована.

Должно быть writeback = !cli->get_immediate_commit(inode).

Эффект: При immediate_commit=none/small (типичная конфигурация) VOLATILE_CACHE=0 — ядро считает устройство немедленно-персистентным, не шлёт FLUSH. Записи в журнал OSD не синкаются. Потеря данных при крахе OSD/питания.

Бонус: на строке 285 в режиме image inode ещё равен 0 (INODE_POOL(0)=0 → get_immediate_commit возвращает true через if (!pool_id) return true;).


2. CRITICAL — Use-after-free в handle_flush_op через stop_client

Файл: src/osd/osd_flush.cpp:67-103

fb->flush_done++;
...
if (op->reply.hdr.retval != 0) {
    msgr.stop_client(peer_it->second->client_id);  // СИНХРОННО → repeer_pgs → reset_pg → delete fb
}
if (fb->flush_done == fb->flush_ops)  // ← UAF

stop_client синхронно вызывает msgr_stop.cpp:123 → repeer_pgs → repeer_pg → reset_pg (osd_peering.cpp:154-158), который делает delete pg.flush_batch; pg.flush_batch = NULL;. После возврата handle_flush_op обращается к fb (= delete'd).

Триггер: любая ошибка от peer-OSD во время flush (EIO/EROFS/EPIPE).

Фикс: отложить stop_client через ringloop->set_immediate(...) или завершить flush_done-проверку до stop_client.


3. HIGH — submit_pg_flush_ops преждевременно завершает batch при отсутствии peer

Файл: src/osd/osd_flush.cpp:47-65, 158-228

fb->flush_ops++ инкрементируется перед submit_flush_op. В submit_flush_op при отсутствии peer:

op->reply.hdr.retval = -EPIPE;
op->callback(op);   // СИНХРОННО → handle_flush_op → fb->flush_done++
return false;

Если это первая op в цикле, flush_done == flush_ops сразу → batch «завершён» через delete fb. После возврата submit_pg_flush_ops видит return false и прекращает отправку остальных rollback/stable_lists.

Аналогичная проблема была закрыта в submit_primary_subop:284 и submit_primary_del_batch:630 через ringloop->set_immediate(...). Здесь забыто.


4. HIGH — submit_primary_sync_subops hung forever если все peers недоступны

Файл: src/osd/osd_primary_subops.cpp:636-687

op_data->n_subops = n_osds;
for (int i = 0; i < n_osds; i++) {
    osd_num_t sync_osd = op_data->dirty_osds[i];
    if (sync_osd == this->osd_num) { bs->enqueue_op(...); }
    else if ((peer_it = msgr.osd_peers.find(sync_osd)) != msgr.osd_peers.end()) { msgr.outbox_push(...); }
    else {
        op_data->done++;   // ← НЕ вызывает callback, st не продвигается
    }
}

Если ВСЕ sync_osd недоступны, done == n_subops сразу. Но continue_primary_sync resume_3 видит n_subops > 0, ставит st = 3 и return. Никакой callback не разбудит — sync застрял навсегда. Все последующие syncs блокируются через syncs_in_progress.

Сравните с submit_primary_stab_subops (стр. 743-748), где правильно вызывается ringloop->set_immediate(...) для отсутствующих peers.


5. HIGH — pick_next_recovery не проверяет scrub_ops и write_queue

Файл: src/osd/osd_flush.cpp:233-289

if (recovery_ops.find(obj_it->first) == recovery_ops.end()) { ... }

Сравните с osd_scrub.cpp:144-146, где корректно проверяются три: recovery_ops, scrub_ops, write_queue. Recovery может подобрать объект, который сейчас scrubbing → одновременные scrub-read и recovery-write на один oid → scrub может ложно пометить объект corrupted.


6. HIGH — pgs.at() throw при stop_pg во время sync

Файл: src/osd/osd_primary_sync.cpp:178-188

auto & pg = pgs.at(dirty_pg_num);   // throws std::out_of_range

Если PG исчезла во время sync (stop_pg, reshard), pgs.at бросит исключение → крах OSD.


7. CRITICAL — NFS-KV setattr (truncate) orphan data → чтение старых данных

Файл: src/nfs/nfs_kv_setattr.cpp:125-145

Алгоритм:

  1. CAS обновление inode metadata с новым size.
  2. Async rm_data для удаления данных за пределами нового размера.

Если шаг 2 падает (некоторые PG недоступны), inode говорит size=N, но объекты в диапазоне (N, M] остаются на дисках.

При последующем расширении файла через setattr size=M' (M'>N), чтение в [N, M'] вернёт старые данные вместо нулей. Нарушение POSIX-семантики truncate, утечка данных предыдущего пользователя.


8. HIGH — Нет обработки SIGTERM в NFS/NBD/UBLK демонах

Файлы: nfs_proxy.cpp:318-322, nbd_proxy.cpp:695-707, ublk_server.cpp:347-356

Ни один демон не устанавливает signal handler. При systemctl stop / k8s SIGTERM процесс умирает мгновенно без cli->flush(). Writeback-кеш с грязными данными теряется.

С учётом UBLK #1 эффект ещё хуже — ядро не шлёт FLUSH, считая кеш отсутствующим.


9. HIGH — QEMU vitastor_co_truncate silent no-op

Файл: src/client/qemu_driver.c:772-799

// TODO: Resize inode to <offset> bytes
client->size = exact || client->size < offset ? offset : client->size;
return 0;

qemu-img resize или TRIM из VM возвращают success, но реальный inode в Vitastor не изменяется. Локальный client->size обновляется только в текущей сессии.

При уменьшении размера через QEMU данные за пределами нового size остаются и могут быть прочитаны через другой клиент (NBD, vitastor-cli).


10. MEDIUM — QEMU vitastor_co_block_status некорректный bmp_end

Файл: src/client/qemu_driver.c:1050-1067

uint64_t bmp_pos = (offset-task.offset) / task.bitmap_granularity;
uint64_t bmp_end = (offset+bytes-task.offset) / task.bitmap_granularity - bmp_pos;
while (bmp_pos < bmp_end)

bmp_pos — абсолютный индекс, bmp_end — после вычитания bmp_pos это длина. Сравнение bmp_pos < bmp_end неверно когда bmp_pos != 0.

Пример: offset=5000, task.offset=0, granularity=4096 → bmp_pos=1, bmp_end=2. Цикл выполнится 1 раз вместо 2.

Эффект: qemu-img convert с sparse-копированием через QEMU API может пропустить allocated extents → данные не экспортируются.


11. HIGH — vitastor_c_destroy() без flush() теряет writeback

Файл: src/client/vitastor_c.cpp:218-228

delete client->cli вызывает ~cluster_client_t(), который не делает flush. C-API клиенты, не вызывающие vitastor_c_sync() перед destroy, теряют writeback-данные.

QEMU защищён через bdrv_co_flush. fio_cluster.cpp:347-363 — не делает flush перед destroy.


12. HIGH — Monitor: shared-ref mutation в state.pg.history через save_new_pgs_txn

Файлы: mon/mon.js:625-631, mon/pg_gen.js:111-119

В apply_pool_pgs собирается pg_history с shared-ref на in-memory state:

pg_history[pg-1] = this.state.pg.history[pool_id][pg];  // shared ref

В save_new_pgs_txn:

pg_history[i] = pg_history[i] || {};
pg_history[i].osd_sets = pg_history[i].osd_sets || [];
pg_history[i].osd_sets.push(prev_pgs[i]);  // мутирует state.pg.history

Аналог фикса 36d2b562 ("Deep copy merged_history"), но в save_new_pgs_txn. При CAS-конфликте retry может встретить уже мутированную history → накопление osd_sets / рассогласование с etcd.

Фикс: deep copy при формировании pg_history.


13. HIGH — Monitor: scale_pg_count shared refs + push в generate_pool_pgs → broken prev_pgs

Файлы: mon/pg_utils.js:98-112, mon/pg_gen.js:222-223

// pg_utils.js scale_pg_count
prev_pgs[i] = prev_pgs[i % prev_pgs.length];  // shared ref!
// pg_gen.js
for (const pg of prev_pgs)
    while (pg.length < pool_cfg.pg_size)
        pg.push(0);  // мутирует разделяемый массив

Пример: prev_pgs=[[1,2,3]], scale до 3 PG → [[1,2,3],[1,2,3],[1,2,3]] (все три = одна ссылка). При pg_size=5 push добавит 2 нуля три раза в один массив → [1,2,3,0,0,0,0,0,0] (6 нулей) вместо ожидаемых 2.

LP-оптимизатор получает мусорные prev_pgs при scale-up.

Фикс: prev_pgs[i] = [...prev_pgs[i % prev_pgs.length]]; (копия).


14. MEDIUM-HIGH — Monitor: ordered не передан в combinator для EC pools

Файл: mon/pg_gen.js:199-208

combinator: use_rules
    ? new RuleCombinator(folded_tree, rules, pool_cfg.max_osd_combinations)   // ordered=undefined
    : new SimpleCombinator(flatten_tree(...), pool_cfg.pg_size, pool_cfg.max_osd_combinations),
pg_count: pool_cfg.pg_count,
...
ordered: pool_cfg.scheme != 'replicated',

Конструкторы Combinator принимают ordered параметром, но он не передаётся (undefined → falsy). Внутри random_custom_combinations это даёт отсортированные комбинации, без ротаций ролей. Для EC pools LP-оптимизатор получает уменьшенное число valid combinations → возможна неравномерность EC ролей по OSD.

В тестах (test-optimize-fold.js:92) ordered: false для EC передаётся явно. В прод-коде забыто.


15. HIGH — Monitor: max_txn_ops=128 лимит etcd блокирует крупные пулы

Файл: mon/pg_gen.js:121-148 (FIXME уже в коде)

При >60 PG в пуле сумма ops в save_pg_config превышает etcd max_txn_ops (default 128). Транзакция отвергается → recheck бесконечно retry → PG конфигурация не сохраняется → пул stuck.

Видимо, нужно разбить на несколько txn.


16. HIGH — Blockstore heap: assert в recheck_buffer при mixed STABLE/UNSTABLE chain

Файл: src/blockstore/blockstore_heap.cpp:909

while (li && cwr != &li->entry) {
    assert(li->entry.entry_type == cwr->entry_type);  // 0x42 != 0x02
    ...
}

add_small_write (blockstore_heap.cpp:1554-1584) разрешает добавлять UNSTABLE small_write поверх STABLE small_write — отбрасывает только обратное ((type & STABLE) && !(obj->entry_type & STABLE) → EINVAL).

Если на рестарте checksum mismatch на нижнем STABLE entry, recheck_buffer обходит head (UNSTABLE) → assert падает. OSD не стартует.


17. MEDIUM — Blockstore heap: ENOENT из add_commit приводит к assert

Файл: src/blockstore/blockstore_stable.cpp:31-59

int res = ... heap->add_commit(...);
if (res == EBUSY) { ... }
if (res == ENOSPC) { ... }
assert(res == 0);  // ← падает при ENOENT

add_commit (blockstore_heap.cpp:1798-1800) возвращает ENOENT если запрашиваемая version не найдена. dequeue_stable обрабатывает только EBUSY/ENOSPC. Bad client request → assert → крах OSD.


18. HIGH — Cluster client: repeat_ops_for не разделяет WRITE/DELETE буферы (latent)

Файл: src/client/cluster_client_wb.cpp:281-283

При флэше группы из WRITE и OSD_OP_DELETE с len>0 в одной flush-операции add_iov упадёт на assert. Сегодня недостижимо (len=0 для DELETE), но мина при первой же реализации TRIM/punch-hole на DELETE — а ты сам отмечал, что хочешь буферизовать удаления.


Blockstore v1 (legacy) — критические баги

В issue #65 упоминались два, всё ещё не исправлены:

v1-1. CRITICAL — Нет проверки CRC chain первой entry журнала

Файл: src/blockstore/v1/init.cpp:777-778

... !started && je->crc32_prev != crc32_last

started=false для первой записи. crc32_prev первой entry не валидируется → можно принять остатки старого журнала после неполного wraparound.

v1-2. CRITICAL — Нет per-block recheck BIG_WRITE при старте v1

Файл: src/blockstore/v1/init.cpp:1010-1112

JE_BIG_WRITE принимается без чтения и валидации данных на data-устройстве. Для small_write валидация есть; для big_write нет. Heap-blockstore делает recheck_small_writes(). При расщеплении data fsync vs journal fsync или bit-rot — silent corruption.

v1-3. CRITICAL — SIGSEGV при corrupted je->size < 4

Файл: src/blockstore/v1/init.cpp:777 + ondisk_formats.h:141-145

crc32c(0x48674bc7, ((uint8_t*)je)+4, je->size-4)

je->size uint32 — при значении <4 wraparound к ~4 GiB → segfault/OOM. Heap-store проверяет минимальный размер, v1 — нет.

v1-4. HIGH — cfg_journal_size игнорируется в v1

Файл: src/blockstore/v1/open.cpp:147

v1 вызывает только dsk.calc_lengths(), но не dsk.check_lengths() (heap вызывает оба). cfg_journal_size > 0 не подрезает journal_len, проверка MIN_JOURNAL_SIZE пропущена. При inmemory_journal=true → huge allocation.

v1-5. HIGH — cancel_all_writes → assert при ENOSPC race

Файл: src/blockstore/v1/write.cpp:217-259

dirty_db.find(...) → assert(... != dirty_db.end()) падает раньше проверки cancellation marker real_version == UINT64_MAX. Крах OSD при cancel-сценарии.

v1-bonus — disk_tool_resize.cpp:592 при HEAP→V1 migration

fill_old_journal_entry жёстко проставляет je->type = JE_SMALL_WRITE_INSTANT/JE_SMALL_WRITE даже для big_write entries. После миграции HEAP→V1 recovery поломан.


Полный отчёт раунда 2 с дополнительными MEDIUM/LOW находками — могу выложить отдельно, если интересно.

Спасибо за быстрые фиксы. Проверил все 13 коммитов между `4b7d0ce2..6d7a7ae8` — все корректны. Прогнали ещё один раунд на коммите `6d7a7ae8`, нашли новые места. Ниже только то, что ещё не обсуждали. ## ⚠️ Возражение по поводу OBJ_CORRUPTED-only (#11 в прошлом списке) Ты ответил: «не бывает объектов в стейте только OBJ_CORRUPTED, оно всегда с MISPLACED/DEGRADED/INCOMPLETE/INCONSISTENT». Перепроверили — это утверждение неверно для EC. В `osd_primary.cpp:514-544`: ```cpp if (n_corrupted > 0) { obj_state |= OBJ_CORRUPTED; pg_state_bits |= PG_HAS_CORRUPTED; } if (n_invalid > 0 || n_inconsistent > 0) { obj_state |= OBJ_INCONSISTENT; } else if (n_roles < pg.pg_data_size) { obj_state |= OBJ_INCOMPLETE; } else if (n_roles < pg.pg_cursize) { obj_state |= OBJ_DEGRADED; } else if (n_misplaced > 0 || n_outdated > 0) { obj_state |= OBJ_MISPLACED; } ``` **Сценарий:** EC pg_size=3, pg_data_size=2. Объект имеет все 3 chunks на cur_set. Scrub помечает parity (role=2) как corrupted: - `n_corrupted = 1` - `n_roles = 2` (роли 0 и 1 целые) — НЕ меньше pg_data_size (2) - `n_misplaced = 0` (corrupted чанки не считаются как misplaced) - `n_outdated = 0`, `n_inconsistent = 0`, `n_invalid = 0` → `obj_state == OBJ_CORRUPTED` (только этот флаг). Затем `remove_object_from_state` (`osd_primary.cpp:631-685`) проходит `corrupted_objects--` в первом блоке, потом `else if (INCONSISTENT/INCOMPLETE/DEGRADED/MISPLACED)` все false → финальный `else throw runtime_error("BUG: Invalid object state: ...")` → краш OSD при следующем write/delete на этот объект. Воспроизводимо в EC-пуле при partial corruption на parity-чанке. --- ## Новые критические находки (HEAD `6d7a7ae8`) ### 1. CRITICAL — UBLK: инвертированный `UBLK_ATTR_VOLATILE_CACHE` → потеря данных при IMMEDIATE_NONE **Файл:** `src/client/ublk_server.cpp:285, 318` ```cpp const bool writeback = cli->get_immediate_commit(inode); ... add_device(req_dev_num, (writeback ? UBLK_ATTR_VOLATILE_CACHE : 0) | ...); ``` `get_immediate_commit()` возвращает `true` когда писания **немедленно персистентны** (нет кеша). `UBLK_ATTR_VOLATILE_CACHE` должен значить **есть** волатильный кеш (требует FLUSH). Семантика инвертирована. Должно быть `writeback = !cli->get_immediate_commit(inode)`. **Эффект:** При `immediate_commit=none/small` (типичная конфигурация) `VOLATILE_CACHE=0` — ядро считает устройство немедленно-персистентным, **не шлёт FLUSH**. Записи в журнал OSD не синкаются. Потеря данных при крахе OSD/питания. Бонус: на строке 285 в режиме image `inode` ещё равен 0 (`INODE_POOL(0)=0` → `get_immediate_commit` возвращает true через `if (!pool_id) return true;`). --- ### 2. CRITICAL — Use-after-free в `handle_flush_op` через stop_client **Файл:** `src/osd/osd_flush.cpp:67-103` ```cpp fb->flush_done++; ... if (op->reply.hdr.retval != 0) { msgr.stop_client(peer_it->second->client_id); // СИНХРОННО → repeer_pgs → reset_pg → delete fb } if (fb->flush_done == fb->flush_ops) // ← UAF ``` `stop_client` синхронно вызывает `msgr_stop.cpp:123 → repeer_pgs → repeer_pg → reset_pg` (`osd_peering.cpp:154-158`), который делает `delete pg.flush_batch; pg.flush_batch = NULL;`. После возврата `handle_flush_op` обращается к `fb` (= delete'd). Триггер: любая ошибка от peer-OSD во время flush (EIO/EROFS/EPIPE). **Фикс:** отложить stop_client через `ringloop->set_immediate(...)` или завершить flush_done-проверку до stop_client. --- ### 3. HIGH — `submit_pg_flush_ops` преждевременно завершает batch при отсутствии peer **Файл:** `src/osd/osd_flush.cpp:47-65`, `158-228` `fb->flush_ops++` инкрементируется перед `submit_flush_op`. В `submit_flush_op` при отсутствии peer: ```cpp op->reply.hdr.retval = -EPIPE; op->callback(op); // СИНХРОННО → handle_flush_op → fb->flush_done++ return false; ``` Если это первая op в цикле, `flush_done == flush_ops` сразу → batch «завершён» через `delete fb`. После возврата `submit_pg_flush_ops` видит `return false` и **прекращает отправку остальных** rollback/stable_lists. Аналогичная проблема была закрыта в `submit_primary_subop:284` и `submit_primary_del_batch:630` через `ringloop->set_immediate(...)`. Здесь забыто. --- ### 4. HIGH — `submit_primary_sync_subops` hung forever если все peers недоступны **Файл:** `src/osd/osd_primary_subops.cpp:636-687` ```cpp op_data->n_subops = n_osds; for (int i = 0; i < n_osds; i++) { osd_num_t sync_osd = op_data->dirty_osds[i]; if (sync_osd == this->osd_num) { bs->enqueue_op(...); } else if ((peer_it = msgr.osd_peers.find(sync_osd)) != msgr.osd_peers.end()) { msgr.outbox_push(...); } else { op_data->done++; // ← НЕ вызывает callback, st не продвигается } } ``` Если ВСЕ sync_osd недоступны, `done == n_subops` сразу. Но `continue_primary_sync` resume_3 видит `n_subops > 0`, ставит `st = 3` и `return`. Никакой callback не разбудит — sync застрял навсегда. Все последующие syncs блокируются через `syncs_in_progress`. Сравните с `submit_primary_stab_subops` (стр. 743-748), где правильно вызывается `ringloop->set_immediate(...)` для отсутствующих peers. --- ### 5. HIGH — `pick_next_recovery` не проверяет `scrub_ops` и `write_queue` **Файл:** `src/osd/osd_flush.cpp:233-289` ```cpp if (recovery_ops.find(obj_it->first) == recovery_ops.end()) { ... } ``` Сравните с `osd_scrub.cpp:144-146`, где корректно проверяются три: `recovery_ops`, `scrub_ops`, `write_queue`. Recovery может подобрать объект, который сейчас scrubbing → одновременные scrub-read и recovery-write на один oid → scrub может ложно пометить объект corrupted. --- ### 6. HIGH — `pgs.at()` throw при stop_pg во время sync **Файл:** `src/osd/osd_primary_sync.cpp:178-188` ```cpp auto & pg = pgs.at(dirty_pg_num); // throws std::out_of_range ``` Если PG исчезла во время sync (stop_pg, reshard), `pgs.at` бросит исключение → крах OSD. --- ### 7. CRITICAL — NFS-KV setattr (truncate) orphan data → чтение старых данных **Файл:** `src/nfs/nfs_kv_setattr.cpp:125-145` Алгоритм: 1. CAS обновление inode metadata с новым size. 2. Async `rm_data` для удаления данных за пределами нового размера. Если шаг 2 падает (некоторые PG недоступны), inode говорит `size=N`, но объекты в диапазоне `(N, M]` остаются на дисках. **При последующем расширении файла** через `setattr size=M' (M'>N)`, чтение в `[N, M']` вернёт **старые данные** вместо нулей. Нарушение POSIX-семантики truncate, утечка данных предыдущего пользователя. --- ### 8. HIGH — Нет обработки SIGTERM в NFS/NBD/UBLK демонах **Файлы:** `nfs_proxy.cpp:318-322`, `nbd_proxy.cpp:695-707`, `ublk_server.cpp:347-356` Ни один демон не устанавливает signal handler. При `systemctl stop` / k8s SIGTERM процесс умирает мгновенно без `cli->flush()`. Writeback-кеш с грязными данными теряется. С учётом UBLK #1 эффект ещё хуже — ядро не шлёт FLUSH, считая кеш отсутствующим. --- ### 9. HIGH — QEMU `vitastor_co_truncate` silent no-op **Файл:** `src/client/qemu_driver.c:772-799` ```c // TODO: Resize inode to <offset> bytes client->size = exact || client->size < offset ? offset : client->size; return 0; ``` `qemu-img resize` или TRIM из VM **возвращают success**, но реальный inode в Vitastor не изменяется. Локальный `client->size` обновляется только в текущей сессии. При уменьшении размера через QEMU данные за пределами нового size остаются и могут быть прочитаны через другой клиент (NBD, vitastor-cli). --- ### 10. MEDIUM — QEMU `vitastor_co_block_status` некорректный bmp_end **Файл:** `src/client/qemu_driver.c:1050-1067` ```c uint64_t bmp_pos = (offset-task.offset) / task.bitmap_granularity; uint64_t bmp_end = (offset+bytes-task.offset) / task.bitmap_granularity - bmp_pos; while (bmp_pos < bmp_end) ``` `bmp_pos` — абсолютный индекс, `bmp_end` — после вычитания `bmp_pos` это длина. Сравнение `bmp_pos < bmp_end` неверно когда `bmp_pos != 0`. Пример: `offset=5000, task.offset=0, granularity=4096` → `bmp_pos=1, bmp_end=2`. Цикл выполнится 1 раз вместо 2. Эффект: `qemu-img convert` с sparse-копированием через QEMU API может пропустить allocated extents → данные не экспортируются. --- ### 11. HIGH — `vitastor_c_destroy()` без flush() теряет writeback **Файл:** `src/client/vitastor_c.cpp:218-228` `delete client->cli` вызывает `~cluster_client_t()`, который **не** делает flush. C-API клиенты, не вызывающие `vitastor_c_sync()` перед destroy, теряют writeback-данные. QEMU защищён через `bdrv_co_flush`. fio_cluster.cpp:347-363 — не делает flush перед destroy. --- ### 12. HIGH — Monitor: shared-ref mutation в `state.pg.history` через `save_new_pgs_txn` **Файлы:** `mon/mon.js:625-631`, `mon/pg_gen.js:111-119` В `apply_pool_pgs` собирается `pg_history` с shared-ref на in-memory state: ```js pg_history[pg-1] = this.state.pg.history[pool_id][pg]; // shared ref ``` В `save_new_pgs_txn`: ```js pg_history[i] = pg_history[i] || {}; pg_history[i].osd_sets = pg_history[i].osd_sets || []; pg_history[i].osd_sets.push(prev_pgs[i]); // мутирует state.pg.history ``` Аналог фикса `36d2b562` ("Deep copy merged_history"), но в `save_new_pgs_txn`. При CAS-конфликте retry может встретить уже мутированную history → накопление osd_sets / рассогласование с etcd. **Фикс:** deep copy при формировании `pg_history`. --- ### 13. HIGH — Monitor: `scale_pg_count` shared refs + push в `generate_pool_pgs` → broken prev_pgs **Файлы:** `mon/pg_utils.js:98-112`, `mon/pg_gen.js:222-223` ```js // pg_utils.js scale_pg_count prev_pgs[i] = prev_pgs[i % prev_pgs.length]; // shared ref! ``` ```js // pg_gen.js for (const pg of prev_pgs) while (pg.length < pool_cfg.pg_size) pg.push(0); // мутирует разделяемый массив ``` Пример: `prev_pgs=[[1,2,3]]`, scale до 3 PG → `[[1,2,3],[1,2,3],[1,2,3]]` (все три = одна ссылка). При pg_size=5 push добавит 2 нуля три раза в один массив → `[1,2,3,0,0,0,0,0,0]` (6 нулей) вместо ожидаемых 2. LP-оптимизатор получает мусорные prev_pgs при scale-up. **Фикс:** `prev_pgs[i] = [...prev_pgs[i % prev_pgs.length]];` (копия). --- ### 14. MEDIUM-HIGH — Monitor: `ordered` не передан в combinator для EC pools **Файл:** `mon/pg_gen.js:199-208` ```js combinator: use_rules ? new RuleCombinator(folded_tree, rules, pool_cfg.max_osd_combinations) // ordered=undefined : new SimpleCombinator(flatten_tree(...), pool_cfg.pg_size, pool_cfg.max_osd_combinations), pg_count: pool_cfg.pg_count, ... ordered: pool_cfg.scheme != 'replicated', ``` Конструкторы Combinator принимают `ordered` параметром, но он не передаётся (undefined → falsy). Внутри `random_custom_combinations` это даёт отсортированные комбинации, без ротаций ролей. Для EC pools LP-оптимизатор получает уменьшенное число valid combinations → возможна неравномерность EC ролей по OSD. В тестах (`test-optimize-fold.js:92`) `ordered: false` для EC передаётся явно. В прод-коде забыто. --- ### 15. HIGH — Monitor: `max_txn_ops=128` лимит etcd блокирует крупные пулы **Файл:** `mon/pg_gen.js:121-148` (FIXME уже в коде) При >60 PG в пуле сумма ops в `save_pg_config` превышает etcd `max_txn_ops` (default 128). Транзакция отвергается → recheck бесконечно retry → PG конфигурация не сохраняется → пул stuck. Видимо, нужно разбить на несколько txn. --- ### 16. HIGH — Blockstore heap: assert в `recheck_buffer` при mixed STABLE/UNSTABLE chain **Файл:** `src/blockstore/blockstore_heap.cpp:909` ```cpp while (li && cwr != &li->entry) { assert(li->entry.entry_type == cwr->entry_type); // 0x42 != 0x02 ... } ``` `add_small_write` (`blockstore_heap.cpp:1554-1584`) разрешает добавлять UNSTABLE small_write поверх STABLE small_write — отбрасывает только обратное (`(type & STABLE) && !(obj->entry_type & STABLE)` → EINVAL). Если на рестарте checksum mismatch на нижнем STABLE entry, `recheck_buffer` обходит head (UNSTABLE) → assert падает. OSD не стартует. --- ### 17. MEDIUM — Blockstore heap: ENOENT из `add_commit` приводит к assert **Файл:** `src/blockstore/blockstore_stable.cpp:31-59` ```cpp int res = ... heap->add_commit(...); if (res == EBUSY) { ... } if (res == ENOSPC) { ... } assert(res == 0); // ← падает при ENOENT ``` `add_commit` (`blockstore_heap.cpp:1798-1800`) возвращает ENOENT если запрашиваемая version не найдена. dequeue_stable обрабатывает только EBUSY/ENOSPC. Bad client request → assert → крах OSD. --- ### 18. HIGH — Cluster client: `repeat_ops_for` не разделяет WRITE/DELETE буферы (latent) **Файл:** `src/client/cluster_client_wb.cpp:281-283` При флэше группы из WRITE и `OSD_OP_DELETE` с `len>0` в одной flush-операции `add_iov` упадёт на assert. Сегодня недостижимо (`len=0` для DELETE), но мина при первой же реализации TRIM/punch-hole на DELETE — а ты сам отмечал, что хочешь буферизовать удаления. --- ## Blockstore v1 (legacy) — критические баги В issue #65 упоминались два, всё ещё не исправлены: ### v1-1. CRITICAL — Нет проверки CRC chain первой entry журнала **Файл:** `src/blockstore/v1/init.cpp:777-778` ```cpp ... !started && je->crc32_prev != crc32_last ``` `started=false` для первой записи. `crc32_prev` первой entry не валидируется → можно принять остатки старого журнала после неполного wraparound. ### v1-2. CRITICAL — Нет per-block recheck BIG_WRITE при старте v1 **Файл:** `src/blockstore/v1/init.cpp:1010-1112` JE_BIG_WRITE принимается без чтения и валидации данных на data-устройстве. Для small_write валидация есть; для big_write нет. Heap-blockstore делает `recheck_small_writes()`. При расщеплении data fsync vs journal fsync или bit-rot — silent corruption. ### v1-3. CRITICAL — SIGSEGV при corrupted `je->size < 4` **Файл:** `src/blockstore/v1/init.cpp:777` + `ondisk_formats.h:141-145` ```c crc32c(0x48674bc7, ((uint8_t*)je)+4, je->size-4) ``` `je->size` uint32 — при значении <4 wraparound к ~4 GiB → segfault/OOM. Heap-store проверяет минимальный размер, v1 — нет. ### v1-4. HIGH — `cfg_journal_size` игнорируется в v1 **Файл:** `src/blockstore/v1/open.cpp:147` v1 вызывает только `dsk.calc_lengths()`, но не `dsk.check_lengths()` (heap вызывает оба). `cfg_journal_size > 0` не подрезает `journal_len`, проверка `MIN_JOURNAL_SIZE` пропущена. При `inmemory_journal=true` → huge allocation. ### v1-5. HIGH — `cancel_all_writes` → assert при ENOSPC race **Файл:** `src/blockstore/v1/write.cpp:217-259` `dirty_db.find(...)` → `assert(... != dirty_db.end())` падает раньше проверки cancellation marker `real_version == UINT64_MAX`. Крах OSD при cancel-сценарии. ### v1-bonus — `disk_tool_resize.cpp:592` при HEAP→V1 migration `fill_old_journal_entry` жёстко проставляет `je->type = JE_SMALL_WRITE_INSTANT/JE_SMALL_WRITE` даже для big_write entries. После миграции HEAP→V1 recovery поломан. --- Полный отчёт раунда 2 с дополнительными MEDIUM/LOW находками — могу выложить отдельно, если интересно.
Owner

Сценарий: EC pg_size=3, pg_data_size=2. Объект имеет все 3 chunks на cur_set. Scrub помечает parity (role=2) как corrupted:
n_corrupted = 1
n_roles = 2 (роли 0 и 1 целые) — НЕ меньше pg_data_size (2)
n_misplaced = 0 (corrupted чанки не считаются как misplaced)
n_outdated = 0, n_inconsistent = 0, n_invalid = 0

А degraded почему пропускаем. Он тут будет)

  1. CRITICAL — UBLK: инвертированный UBLK_ATTR_VOLATILE_CACHE → потеря данных при IMMEDIATE_NONE

Точно, исправил

  1. CRITICAL — Use-after-free в handle_flush_op через stop_client

Да, исправил, правда не так как описано, правильно просто выйти

  1. HIGH — submit_pg_flush_ops преждевременно завершает batch при отсутствии peer

Тут всё правильно, при return false оно сразу дёрнет неудачный колбэк, остановит и дальше по пункту 2

  1. HIGH — submit_primary_sync_subops hung forever если все peers недоступны

Ну, блин, а это между прочим после предыдущего исправления по отчёту AI) аккуратненько надо однако с вашими отчётами. Поправил.

  1. HIGH — pick_next_recovery не проверяет scrub_ops и write_queue

Тут проблем нет, recovery само встаёт в write_queue, а scrub допускает параллельную работу с записью и ничего не промаркирует - сверяет версии

  1. HIGH — pgs.at() throw при stop_pg во время sync

Не, там есть защита через pg.inflight. Во время sync pg не может stop.

  1. CRITICAL — NFS-KV setattr (truncate)
    Если шаг 2 падает (некоторые PG недоступны), inode говорит size=N, но объекты в диапазоне (N, M] остаются на дисках.

Ну такое. Думаю оставлю как есть, не кажется критичным. Файл же самому пользователю и принадлежит, условно говоря.

  1. HIGH — Нет обработки SIGTERM в NFS/NBD/UBLK демонах

Ну наверное стоит добавить. Пока не добавлял.

  1. HIGH — QEMU vitastor_co_truncate silent no-op

Так и задумано.

  1. MEDIUM — QEMU vitastor_co_block_status некорректный bmp_end

Вот это кстати важнее чем предыдущие. Поправил.

  1. HIGH — vitastor_c_destroy() без flush() теряет writeback

Ну возможно и стоит добавить flush в деструктор для всех клиентов. Хотя именно касаемо fio пофигу, оно для бенчей и там есть опция -end_fsync, если надо.

  1. HIGH — Monitor: shared-ref mutation в state.pg.history через save_new_pgs_txn

Хз влияет ли на что-то но добавил deep copy, хуже не будет.

  1. HIGH — Monitor: scale_pg_count shared refs + push в generate_pool_pgs → broken prev_pgs

Ну вот здесь точно пофиг, нулей там лишних точно не появится. Но с другой стороны почему бы и нет, добавил [...]. :)

  1. MEDIUM-HIGH — Monitor: ordered не передан в combinator для EC pools

Тут надо ещё проверить, есть вариант что это специально. Но это не точно :)

  1. HIGH — Monitor: max_txn_ops=128 лимит etcd блокирует крупные пулы

С дефолтным лимитом vitastor просто и не работает, это нормально.

  1. HIGH — Blockstore heap: assert в recheck_buffer при mixed STABLE/UNSTABLE chain

Ничего особо не затрагивает, но для порядка поправил.

  1. MEDIUM — Blockstore heap: ENOENT из add_commit приводит к assert

Вроде разумно, добавил обработку. Ненормальная ситуация (значит версия которую пытаемся коммитить не существует), но лучше в ней не падать.

  1. HIGH — Cluster client: repeat_ops_for не разделяет WRITE/DELETE буферы (latent)

Сейчас неактуально в силу того что неактуальна буферизация удалений, да.

v1-1. CRITICAL — Нет проверки CRC chain первой entry журнала

Не должна она проверяться, у неё crc chain всегда указывает на несуществующую запись. Так и должно быть.

v1-2. CRITICAL — Нет per-block recheck BIG_WRITE при старте v1

Не, по алгоритму старого стора и не должно быть никакого recheck big_write. Там строго доверяем ордерингу fsync-ов.

v1-3. CRITICAL — SIGSEGV при corrupted je->size < 4

Добавил проверочку, для обработки корапченного диска разумно.

v1-4. HIGH — cfg_journal_size игнорируется в v1

Для меты разница в вызовах тут точно специально, поэтому просто добавлять check_lengths точно нельзя. Так что наверное трабл только в том что не проверится слишком маленький размер журнала... но это в общем пофигу. Просто стор потом подвиснет быстро, писать журнал некуда будет.

v1-5. HIGH — cancel_all_writes → assert при ENOSPC race

Что-то вообще не понял о чём речь, проверка real_version точно идёт до cancel_all_writes.

v1-bonus — disk_tool_resize.cpp:592 при HEAP→V1 migration

Вообще выпилил эту конверсию из нового стора в старый, она не тестировалась и не используется. Легче если понадобится из истории вернуть :)

>Сценарий: EC pg_size=3, pg_data_size=2. Объект имеет все 3 chunks на cur_set. Scrub помечает parity (role=2) как corrupted: >n_corrupted = 1 >n_roles = 2 (роли 0 и 1 целые) — НЕ меньше pg_data_size (2) >n_misplaced = 0 (corrupted чанки не считаются как misplaced) >n_outdated = 0, n_inconsistent = 0, n_invalid = 0 А degraded почему пропускаем. Он тут будет) >1. CRITICAL — UBLK: инвертированный UBLK_ATTR_VOLATILE_CACHE → потеря данных при IMMEDIATE_NONE Точно, исправил >2. CRITICAL — Use-after-free в handle_flush_op через stop_client Да, исправил, правда не так как описано, правильно просто выйти >3. HIGH — submit_pg_flush_ops преждевременно завершает batch при отсутствии peer Тут всё правильно, при return false оно сразу дёрнет неудачный колбэк, остановит и дальше по пункту 2 >4. HIGH — submit_primary_sync_subops hung forever если все peers недоступны Ну, блин, а это между прочим после предыдущего исправления по отчёту AI) аккуратненько надо однако с вашими отчётами. Поправил. >5. HIGH — pick_next_recovery не проверяет scrub_ops и write_queue Тут проблем нет, recovery само встаёт в write_queue, а scrub допускает параллельную работу с записью и ничего не промаркирует - сверяет версии >6. HIGH — pgs.at() throw при stop_pg во время sync Не, там есть защита через pg.inflight. Во время sync pg не может stop. >7. CRITICAL — NFS-KV setattr (truncate) >Если шаг 2 падает (некоторые PG недоступны), inode говорит size=N, но объекты в диапазоне (N, M] остаются на дисках. Ну такое. Думаю оставлю как есть, не кажется критичным. Файл же самому пользователю и принадлежит, условно говоря. >8. HIGH — Нет обработки SIGTERM в NFS/NBD/UBLK демонах Ну наверное стоит добавить. Пока не добавлял. >9. HIGH — QEMU vitastor_co_truncate silent no-op Так и задумано. >10. MEDIUM — QEMU vitastor_co_block_status некорректный bmp_end Вот это кстати важнее чем предыдущие. Поправил. >11. HIGH — vitastor_c_destroy() без flush() теряет writeback Ну возможно и стоит добавить flush в деструктор для всех клиентов. Хотя именно касаемо fio пофигу, оно для бенчей и там есть опция -end_fsync, если надо. >12. HIGH — Monitor: shared-ref mutation в state.pg.history через save_new_pgs_txn Хз влияет ли на что-то но добавил deep copy, хуже не будет. >13. HIGH — Monitor: scale_pg_count shared refs + push в generate_pool_pgs → broken prev_pgs Ну вот здесь точно пофиг, нулей там лишних точно не появится. Но с другой стороны почему бы и нет, добавил [...]. :) >14. MEDIUM-HIGH — Monitor: ordered не передан в combinator для EC pools Тут надо ещё проверить, есть вариант что это специально. Но это не точно :) >15. HIGH — Monitor: max_txn_ops=128 лимит etcd блокирует крупные пулы С дефолтным лимитом vitastor просто и не работает, это нормально. >16. HIGH — Blockstore heap: assert в recheck_buffer при mixed STABLE/UNSTABLE chain Ничего особо не затрагивает, но для порядка поправил. >17. MEDIUM — Blockstore heap: ENOENT из add_commit приводит к assert Вроде разумно, добавил обработку. Ненормальная ситуация (значит версия которую пытаемся коммитить не существует), но лучше в ней не падать. >18. HIGH — Cluster client: repeat_ops_for не разделяет WRITE/DELETE буферы (latent) Сейчас неактуально в силу того что неактуальна буферизация удалений, да. >v1-1. CRITICAL — Нет проверки CRC chain первой entry журнала Не должна она проверяться, у неё crc chain всегда указывает на несуществующую запись. Так и должно быть. >v1-2. CRITICAL — Нет per-block recheck BIG_WRITE при старте v1 Не, по алгоритму старого стора и не должно быть никакого recheck big_write. Там строго доверяем ордерингу fsync-ов. >v1-3. CRITICAL — SIGSEGV при corrupted je->size < 4 Добавил проверочку, для обработки корапченного диска разумно. >v1-4. HIGH — cfg_journal_size игнорируется в v1 Для меты разница в вызовах тут точно специально, поэтому просто добавлять check_lengths точно нельзя. Так что наверное трабл только в том что не проверится слишком маленький размер журнала... но это в общем пофигу. Просто стор потом подвиснет быстро, писать журнал некуда будет. >v1-5. HIGH — cancel_all_writes → assert при ENOSPC race Что-то вообще не понял о чём речь, проверка real_version точно идёт до cancel_all_writes. >v1-bonus — disk_tool_resize.cpp:592 при HEAP→V1 migration Вообще выпилил эту конверсию из нового стора в старый, она не тестировалась и не используется. Легче если понадобится из истории вернуть :)
Owner

Интересно, в старом сторе нашло мало багов - это потому что плохо искало или потому что он правда отдебаженный?

Интересно, в старом сторе нашло мало багов - это потому что плохо искало или потому что он правда отдебаженный?
Owner

Короче тут осталось

  • Проверить/исправить mark_partial_write для реплик
  • Проверить тест повторов с fsync в test_cluster_client
  • Проверить/исправить scrub с outdated
  • Проверить/опционально добавить проверку lease в запись статистики и last_clean_pgs мониторов
  • Добавить обработку SIGTERM в NFS/NBD/UBLK демонах
  • Добавить flush в деструктор для всех клиентов
  • Проверить нужен ли ordered аргументом RuleCombinator для EC пулов
  • Проверить/вернуть проверку cfg_journal_size в store v1
Короче тут осталось - Проверить/исправить mark_partial_write для реплик - Проверить тест повторов с fsync в test_cluster_client - Проверить/исправить scrub с outdated - Проверить/опционально добавить проверку lease в запись статистики и last_clean_pgs мониторов - Добавить обработку SIGTERM в NFS/NBD/UBLK демонах - Добавить flush в деструктор для всех клиентов - Проверить нужен ли ordered аргументом RuleCombinator для EC пулов - Проверить/вернуть проверку cfg_journal_size в store v1
maxpain changed title from Data integrity review using AI (Claude) v2 to Data integrity review using AI (Claude 4.7) v2 2026-05-29 03:43:44 +03:00
Sign in to join this conversation.
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vitalif/vitastor#67
No description provided.