Data integrity review using AI (Kimi K2.6) #71
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Аудит Vitastor на баги, приводящие к повреждению данных
Дата: 2026-05-25
Аудитор: AI-агент (глубокий параллельный анализ ~25 ключевых подсистем)
Ревизия: текущая
HEADиз/home/maxpain/vitastorExecutive Summary
В ходе максимально глубокого аудита Vitastor на предмет багов, которые в продакшене могут привести к потере или повреждению данных клиентов, были выявлены десятки критических и высоких уязвимостей во всех ключевых компонентах:
PG_PEERING, неправильная маркировка испорченных реплик.force=trueпри подготовке дисков, перезапись живых суперблоков, порча journal'а при resize, удаление данных перед подтверждением metadata CAS.ringloop(UAF, double-free, потеря SQE), integer overflow в таймерах, обнуление 64-битных оффсетов в bitmap до 32-бит.Всего в отчете описано ~50 уникальных багов (после дедупликации). Из них CRITICAL — ~20, HIGH — ~20, остальные MEDIUM/LOW.
Сводная таблица (Summary Matrix)
v1/flush.cppfsync_batchсообщает о завершении fsync до фактического выполнения, нарушая ordering data→metadatablockstore_flush.cppv1/flush.cppsync_to_repeatстирается до декрементаactive_flushers, позволяя двум корутинам одновременно флашить один объектv1/rollback.cpperase_dirtyосвобождает big-write блоки без проверкиused_clean_objects, приводя к чтению чужих данных после переиспользования блокаv1/write.cpprelease_journal_sectorsсдвигаетdirty_startмимо незавершенных секторов, нарушая инварианты журналаv1/sync.cppack_syncиспользуетdirty_db[*it]вместоfind(), при откате до ACK создает фиктивные dirty-записи с состоянием0blockstore_read.cppblockstore_read.cppskip_allотменяет все big-write проверки checksum при смене LSN между чтением и completion, маскируя битротblockstore_read.cppdata->res == 0) на файловом бэкенде трактуется как успех; возвращается неинициализированная памятьblockstore_read.cpp-EIO) перезатирается на-EDOM(checksum mismatch)blockstore_stable.cppENOSPCна multi-object stabilize — бесконечный цикл, весь blockstore зависаетblockstore_impl.cppstop_sync_submittedникогда не сбрасывается;is_safe_to_stop()вечно возвращаетfalse, приводя к force-kill и потере данныхblockstore_sync.cppdata_fd == journal_fdи journal fsync отключен, data fsync молча пропускается; данные теряются при power failv1/init.cppv1/init.cppje->sizeдля конкретного типа; можно спровоцировать OOB-чтение и внедрить фантомные записиblockstore_disk.cppmeta_block_size / clean_entry_size=> деление на 0 при большомdata_block_sizeи маленькомmeta_block_sizeblockstore_disk.cppblockstore_heap.cppmeta_block_countобрезается изuint64_tвuint32_t; при >16TB метаданных счетчик оборачиваетсяosd_primary_write.cpposd_primary_subops.cppmark_object_corrupted()для replicated пулов сопоставляет ошибку чтения с последней репликой в цикле, а не с той, что реально упалаosd_primary_subops.cpposd_primary_subops.cppdirty_osds, они пропадают из tracking; данные могут уйти без fsyncosd_rmw.cppcalc_rmw_parity_xorприpg_minsize == 1не инициализирует parity-буфер; пишется мусор с кучиosd_rmw.cppreconstruct_stripes_xorприpg_minsize == 1не реконструирует недостающий страйп; оставляет неинициализированную памятьosd_rmw.cppwrite_parity * (end-start)(32-bit до приведения к 64-bit); alloc underflow → heap corruptionosd_rmw.cppec_find_good/ec_check_combinationзаписывает за границуtmp_buf, выделенного подstripe_count, но цикл идёт доpg_sizeosd_peering_pg.cppPG_PEERINGникогда не сбрасывается;calc_object_statesвызывается многократно с устаревшимиdegraded_objects/ver_overrideosd_peering_pg.cppcalc_object_statesне очищаетstate_dict,degraded_objects,incomplete_objects,misplaced_objects,ver_overrideперед пересчетомosd_peering_pg.cpposd_peering_pg.cpp!target_ver) молча игнорируются; становятся невидимыми, а последующие записи считают их новымиosd_scrub.cppstripes[chunk.role]при EC scrub, еслиstripe_count < pg_size; портит состояниеLOC_CORRUPTEDosd_flush.cppsubmit_flush_opвозвращаетfalse, оставшиеся flush actions помечаются как submitted и стираются, не будучи отправленнымиosd_flush.cppmsgr_receive.cppuint32_toverflowsec_rw.len + attr_lenпозволяет пропустить чтение payload; в буфер пишется неинициализированный мусор из кучиmsgr_receive.cppmemalign_or_dieбез верхней границы; remote DoS / OOM /exit(1)msgr_receive.cppsizeof(obj_ver_id) * retvalпереполняетint read_remainingвOSD_OP_SEC_LIST; потенциальный desync/крашmsgr_receive.cppexec_opmsgr_send.cppzc_free_listзалипает на ведущемNULL, утекая все будущиеosd_op_tобъектыmsgr_stop.cppforce_delete=trueразрушает клиента, пока в ядре еще висят io_uring SQE; UAF буферовmsgr_rdma.cpp~msgr_rdma_buf_t()вызываетfree(buf)доibv_dereg_mr(mr); RDMA может писать в переиспользованную памятьmsgr_rdma.cpprc->max_msg * rc->max_recvвuint64_tтихо обрезается доsize_tна 32-bit; SGE перекрываютсяmsgr_rdmacm.cpp.at(cmid)безtry/catch;std::out_of_rangeвызываетstd::terminateи немедленный краш OSDcluster_client_wb.cppwriteback_queue_sizeunderflow при interior overlap; автоматический flush перестает запускаться, acknowledged writes теряютсяcluster_client.cppcluster_client_tне вызываетflush(); dirty buffers иwriteback_overflowоперации утекают/теряютсяcluster_client_wb.cpp+cluster_client.cppwriteback_cache_tпри in-flight flushes при уничтожении клиентаcluster_client_wb.cppcluster_client.cppflush()в no-ringloop режиме возвращается до фактического завершения SYNC; данные не гарантированно на дискеcluster_client_wb.cppOP_IMMEDIATE_COMMITсбрасывается при writeback flush; пул с immediate commit теряет durability гарантиюcluster_client.cppcontinue_rw()infinite loop при retry послеPART_SENTошибки;CACHE_REPEATINGbuffer залипает, запись теряетсяcluster_client.cppcontinue_sync()считает SYNC успешным, еслиdirty_osdsочищен и все цели упали; fsync не вызывался ни на одном OSDcluster_client.cppCACHE_REPEATINGbuffer не удаляется; последующий read возвращает незафиксированные (failed) данныеcluster_client.cppinc_wait()обходитretry_after, форсируя немедленный retry; возможна реентерабельность и порча state machinemon/pool_config.jsparity_chunksпри некорректном значенииundefined/0проходит валидацию для EC-пула; объекты пишутся без четностиmon/antietcd_adapter.jsbecome_masterи другие транзакции не используютcompare(CAS); возможен split-brain мониторов и конфликтующие PG mapsmon/mon.js/pg_gen.jsTypeErrorи крашу монитора; кластер не может сгенерировать конфигурациюmon/stats.jsBigInt(float)бросает исключение при дробномosd_backfillfull_ratio; монитора падает в цикле, ребалансировка останавливаетсяmon/osd_tree.jsroot_nodeопустошает дерево OSD; PG генерируются как[0,0,0]и пул полностью недоступенmon/lp_optimizer.jsmake_cyclicсломан (использует string.sizeвместо array.length); EC не оптимизируется, optimizer крашитсяdisk_tool_prepare.cppprepare()жестко выставляетoptions["force"] = true, обходя все проверки и стирая живые OSD без предупрежденияdisk_tool_meta.cppget_ext_bitmapвместоget_checksums; metadata портится при импорте JSONdisk_tool_meta.cppfor (size_t i = jbo.size()-1; i >= 0; i--)— unsigned underflow, бесконечный цикл / OOB на пустом вектореdisk_tool_resize.cppnew_crc32_prevне обновляется после rebuild journal; journal replay отказывается воспроизводить, потеря незафиксированных записейdisk_tool_meta.cppcsum_block_size == 0в JSON metadata; tool мгновенно падает с SIGFPEcli_modify.cppstart_rm_data()вызывается до успешного etcd CAS; при неудаче CAS данные уже уничтожены, а метаданные старыеcli_modify.cpp--force-sizeбез--resizeустанавливаетnew_size=0и вызываетstart_rm_data— полная потеря данных образаcli_pool_rm.cppforce = !cfg["force"].is_null()— присутствие ключаforce: falseвключает force mode; пул удаляется без проверокcli_fix.cpp--check noпропускает проверку консистентности перед удалением копий; можно случайно удалить живые копииcli_rm_data.cppOSD_OP_IGNORE_READONLYна каждом delete без проверки; прRead-only snapshots/image уничтожаются без подтвержденияcli_pool_rm.cppnfs/nfs_kv_write.cppadd_zero(op, st->offset, ...)вместоst->new_size - (st->offset+st->size); tail shared write пишет неправильное количество нулей/мусорnfs/nfs_kv_write.cppnfs_do_fsyncигнорируетop->retval; клиенту возвращаетсяFILE_SYNCдаже если fsync OSD провалился — данные теряютсяnfs/nfs_kv_read.cppretval < len) не трактуется как ошибка; NFS клиент получает неинициализированную память из кучиnfs/nfs_proxy.cppnfs_client_t::refsне учитывает in-flight cluster ops; disconnect приводит кdelete thisпод живыми callback → UAFclient/ublk_server.cppop->lenотносительноmax_io_buf_bytes; kernel ublk request может вызвать heap buffer overflowclient/nbd_proxy.cppreq_typeне маскируетсяNBD_CMD_MASK_COMMAND; flags в верхних битах приводят кexit(1)и потере in-flight writesclient/nbd_proxy.cppclient/nbd_proxy.cppop->len, а неop->retval; tail — неинициализированный мусорclient/qemu_driver.cqemu_aio_release(task)на stack-allocatedVitastorRPC; double-free / memory corruptionclient/qemu_driver.cvitastor_co_block_statusпроверяет покрытие bitmap доoffset+1вместоoffset+bytes; QEMU получает неверную карту блоковclient/qemu_driver.cBDRV_REQ_FUAполностью игнорируется; guest filesystem может повредиться при сбое хостаutil/ringloop.cpp~ring_loop_t()освобождаетring_datas/free_ring_dataдоio_uring_queue_exit; другой thread вloop()→ UAFutil/ringloop.h/.cppsubmit()вызываетio_uring_submit()без захватаmu; multithread race на ядерном ring state → потеря/переупорядочивание SQEutil/ringloop.cpprestore()возвращает slot'ы вfree_ring_dataбез блокировки; race сsubmit()иloop()→ double-free и callback mix-uputil/crc32c.ccrc32c_nopadигнорируетprev_crc, всегда используя0; нарушает инкрементальную проверку checksumutil/timerfd_manager.cppmillis*1000overflow вset_timer; большой таймаут сбрасывается к нулю, lease/peering таймеры срабатывают мгновенноutil/timerfd_manager.cpponstackне сбрасывается при exception в callback; все дальнейшие таймеры перестают стрелять — кластер замираетutil/allocator.cppunsigned(32-bit); диски >4 GiB → truncation, портятся allocator bitmap'ыblockstore_read.cppuint32_toverflowoffset+lenвprepare_read_simple; обход валидации и чтение за пределами блокаosd_rmw.cppsplit_stripesend = start + enduint32_t overflow; страйпы разбиваются неверноosd_rmw.cppmatrices(std::map) без синхронизации; при multithreaded OSD — race и некорректные EC-матрицыmon/mon.jsstop_all_pgsне видит новые OSD без stats; во время pause PG новый OSD подхватывает старую карту и пишет не тудаmon/lp_optimizer.js(1 << old_pg_size)32-bit overflow дляold_pg_size >= 31; неверные веса перебалансировкиdisk_tool_prepare.cpp/udev.cppwrite_zeroиwrite_osd_superblockне вызывают fsync передclose; superblock может не попасть на дискdisk_tool_resize.cppnew_data_lenприnew_data_offset > device_size; resize работает сUINT64_MAXmon/osd_tree.jsroot_nodeопустошает дерево OSD; PG генерируются как[0,0,0]и пул полностью недоступенosd_peering_pg.cpp!target_ver) молча игнорируются; становятся невидимыми, а последующие записи считают их новымиosd_secondary.cpp/msgr_op.cppis_recovery_related()приоритет&&/`osd_flush.cpp→osd_rmw.cppcluster_client_wb.cppop->len > 0приводит к assert == crash; потеря всех buffered writesdisk_tool_meta.cppfromhexstrпишет checksums во внешний bitmap вместо checksum поля; metadata поврежденаДетальное описание по компонентам
1. Blockstore (V1 и V3 / heap)
1.1
v1/flush.cpp—fsync_batchлжет о завершении fsync (CRITICAL)Батчевый fsync использует
flusher_sync_t::state. Если корутина застаетstate == 1(уже отправлено в io_uring, но callback еще не пришел), она считает fsync выполненным и идет дальше. Это нарушает ordering: metadata может быть записана до того, как данные точно окажутся на диске. При power fail читаемся по metadata на несуществующие (еще не записанные) data blocks.1.2
blockstore_flush.cpp— отсутствует fsync финального metadata write (CRITICAL)После normal compaction финальный
write_meta_block(12)не сопровождаетсяfsync_meta(). После краша на диске остается старый metadata, указывающий на блоки, которые уже могли быть переиспользованы. Это приводит к cross-object corruption.1.3
v1/flush.cpp—sync_to_repeatстирается раньше декрементаactive_flushers(HIGH → CRITICAL)Две корутины могут одновременно флашить один объект. Они читают одну и ту же dirty history и пишут metadata повторно/разнонаправленно, что приводит к metadata inconsistency.
1.4
v1/rollback.cpp—erase_dirtyосвобождает big-write блок без проверкиused_clean_objects(CRITICAL)Если на big-write блок приходит in-flight read, rollback помечает блок свободным. Следующий write получает этот же блок, перезаписывает его, а in-flight read возвращает чужие данные. Тихая коррупция данных.
1.5
blockstore_stable.cpp— infinite livelock при ENOSPC (CRITICAL)Multi-object stabilize не обрабатывает
ENOSPCкорректно и зацикливается. Blockstore перестает обрабатывать любой I/O, acknowledged writes висят независимо и в итоге теряются при force-kill.1.6
blockstore_sync.cpp— потеря data fsync приdata_fd == journal_fd(HIGH)Если journal и data на одном устройстве и journal fsync отключен, data fsync молча пропускается. Данные могут лежать в кэше диска. После power loss запись, которую OSD подтвердил, пропадает.
1.7
v1/init.cpp— потеря wrapped small-write при journal replay (CRITICAL)Если small-write данные перенеслись с хвоста журнала на начало (wrap), replay не находит хвостовую часть. Запись молча отбрасывается, объект откатывается к предыдущей версии.
1.8
v1/init.cpp— OOB чтение при replay маленькогоje->size(CRITICAL)Валидация проверяет только
buf_pos + je->size <= len. Еслиje->sizeменьше структуры, поля читаются за пределами буфера. Это позволяет внедрить фантомные stabilize/rollback/delete записи.1.9
blockstore_disk.cpp— деление на 0 и overlap областей (HIGH)При конфигурации больших
data_block_sizeи маленькихmeta_block_sizeпроисходит division by zero. Также отсутствует проверка пересечения data/meta/journal на одном устройстве — метаданные могут перезаписать журнал.1.10
blockstore_read.cpp— in-place compaction перезаписывает старые версии (CRITICAL)Flusher пишет merged small writes на место старого big-write блока. Старый big-write heap entry остается живым до GC. Чтение старой версии получит новые данные. Особенно опасно для EC reconstruction/peering reads.
1.11
blockstore_read.cpp— чтение неинициализированной памяти (HIGH)На обычном файле (не raw device) EOF возвращает
data->res == 0, но код трактует это как success; пользователю отдается мусор из буфера.1.12
blockstore_impl.cpp—is_safe_to_stop()вечный hang (HIGH)Флаг
stop_sync_submittedне сбрасывается. Graceful shutdown невозможен, процесс убивается. In-flight metadata writes обрываются, журнал остается в неконсистентном состоянии.2. OSD (Primary, Subops, RMW, Peering, Recovery)
2.1
osd_primary_subops.cpp—mark_object_corruptedошибочно маркирует живую реплику (CRITICAL)Для replicated пулов все реплики записываются в
stripes[0], перезаписываяosd_num. По завершенииstripes[0].osd_numуказывает на последнюю реплику. Если чтение падает, bad replica не маркируется, а healthy replica помечаетсяLOC_CORRUPTED. Скруб и последующие чтения будут считать эту реплику битой, игнорируя реальную bad replica.2.2
osd_primary_subops.cpp— EC/XOR rollback пропускает отключившиеся реплики (CRITICAL)Если реплика отвалилась между write и rollback, ей rollback не отправляется. На реплике остается "ghost" версия, которая никогда не была стабилизирована. Последующие чтения с этой реплики дадут несогласованные данные.
2.3
osd_primary_subops.cpp— sync молча пропускает offline OSD (HIGH)submit_primary_sync_subopsпропускает OSD, которых нет вosd_peers. Они стираются изdirty_osds. Если такой OSD позже вернется без краша, он покажет клиенту unstabled данные.2.4
osd_rmw.cpp— неинициализированная parity приpg_minsize == 1(CRITICAL)Цикл
for (int other = 0; other < pg_minsize; other++)приpg_minsize == 1(RAID-1-like двухполосный XOR) никогда не выполняет XOR. Parity-буфер остается неинициализированным. Четность пишется на диск как мусор с кучи. При восстановлении из четности — полная коррупция данных.2.5
osd_rmw.cpp— переполнение буфера вec_find_good(HIGH)Во время scrub
stripe_countможет быть меньшеpg_size. Функция выделяетstripe_count * (chunk_size + bitmap_size), но цикл идет доpg_size. Происходит heap buffer overflow.2.6
osd_peering_pg.cpp—PG_PEERINGникогда не сбрасывается (CRITICAL)После первого peering флаг
PG_PEERINGостается. Каждое завершение I/O вызываетcontinue_pg_peering(), который перезапускаетcalc_object_states(). Поскольку maps (state_dict,degraded_objects,ver_override) не очищаются, используются устаревшие записи. Primary читает/пишет по устаревшимread_target, что приводит к перезаписи свежих данных старыми.2.7
osd_peering_pg.cpp—calc_object_statesне очищает maps (CRITICAL)Состояния объектов между пересчетами накапливаются. Объекты, которые давно стали clean, остаются в
degraded_objects. Read/write отправляются на неправильный subset OSD.2.8
osd_peering_pg.cpp— epoch regression (HIGH)Epoch вычисляется только по видимым объектам. Если последние версии были на умершем OSD, epoch может откатиться. Возвращение OSD с более высокой sequence number может "победить" новые записи, откатив их назад.
2.9
osd_peering_pg.cpp— объекты без recoverable версии исчезают из пейджинга (HIGH)Если ни одна версия не удовлетворяет условиям стабильности,
finish_objectвозвращается, не добавляя объект ни в какие maps. Объект становится невидимым для primary. Клиент получаетENOENT. При следующей записи primary считает, что создает новый объект, хотя на OSD уже есть старая, не реконсилированная версия.2.10
osd_scrub.cpp— OOBstripes[chunk.role](CRITICAL)При EC scrub, если не все полосы (chunks) актуальны,
stripe_count<pg_size, ноchunk.role— это настоящий EC role доpg_size-1. Обращениеstripes[chunk.role]выходит за границы массива, читая garbage. Скраб помечает объект как корректный/испорченный неправильно.2.11
osd_flush.cpp— неотправленные flush actions помечаются завершенными (HIGH)Если
submit_flush_opвозвращаетfalse(OSD offline), оставшиеся items в batch уже помеченыsubmitted = trueранее. При последующемhandle_flush_opони стираются изflush_actions, не будучи отправленными. Replica остается с нестабильной версией.2.12
osd_flush.cpp— batch помечается complete при ошибке peer (HIGH)handle_flush_opинкрементируетflush_doneдо проверкиretval. При ошибке он возвращается рано, не декрементируя. Рано или поздноflush_done == flush_ops, batch считается завершенным, PG считается clean, а клиентские записи возобновляются. Но на failed replica версия не стабилизировалась.2.13
osd_rmw.cpp/osd_flush.cpp— XOR-2 recovery пишет неинициализированную четность (CRITICAL)Та же проблема, что и в 2.4, но срабатывает при recovery. Восстановленный объект получает мусорную четность, которая затем используется при чтении.
3. Messenger / Networking
3.1
msgr_receive.cpp—sec_rw.len + attr_lenuint32 overflow (CRITICAL)Сумма двух
uint32_tможет обернуться к 0.read_remaining == 0, payload не читается, операция сразу помечается готовой. В буфер (выделенный наlen) пишется 0 байт из сети, но затем этот буфер используется как данные для записи на диск. В итоге на диск пишется неинициализированная память процесса (мусор, потенциально чувствительные данные).3.2
msgr_receive.cpp— неограниченная аллокация по длине из сети (CRITICAL)Поля длины напрямую передаются в
memalign_or_die/malloc_or_die. Злоумышленник может передатьUINT32_MAX, что приведет к мгновенномуexit(1)из-за OOM или к исчерпанию памяти. Это remote DoS.3.3
msgr_receive.cpp— неизвестные opcodes принимаются (HIGH)Нет
else { reject unknown opcode; }. Неизвестный opcode сread_remaining == 0проходит вexec_opс неинициализированным union payload.3.4
msgr_rdma.cpp— деструктор~msgr_rdma_buf_tfree до dereg (CRITICAL)free(buf)вызывается доibv_dereg_mr(). Другой allocation может получить тот же виртуальный адрес, в который RDMA completion еще может писать.3.5
msgr_rdma.cpp—max_msg * max_recvtruncation на 32-bit (CRITICAL/32-bit)Продукт
uint64_tобрезается доsize_tна 32-bit. SGE ring buffer перекрывается, одновременные записи от NIC коррумпируют данные.3.6
msgr_rdmacm.cpp—.at(cmid)без try/catch (HIGH)std::out_of_rangeвызываетstd::terminate. OSD падает немедленно.4. Client / Writeback Cache
4.1
cluster_client_wb.cpp—writeback_queue_sizeunderflow (CRITICAL)При пересечении записи с dirty run внутри contiguous region
writeback_queue_sizeдекрементируется без корреспондирующего инкремента. Counter становится отрицательным.start_writebacks()больше не запускается (циклwhile wb->writeback_queue_size > maxне срабатывает). Все acknowledged writes остаются в памяти и теряются при краше.4.2
cluster_client.cpp— деструктор не сбрасывает dirty (CRITICAL)~cluster_client_t()удаляетwriteback_cache_tбезflush().dirty_buffersиwriteback_overflowутекают, acknowledged writes пропадают.4.3
cluster_client_wb.cpp— use-after-free при уничтожении клиента (CRITICAL)Flush callbacks захватывают
[this, flush_id]…. Если клиент удален до completion callback, callback пишет в освобожденную память. Это может повредить heap и привести к дальнейшей коррупции.4.4
cluster_client.cpp—flush()не дожидается SYNC (HIGH)В
!ringloop(блокирующем) режимеflush()запускает writebacks, ставит SYNC в очередь и сразу возвращается. Вызывающий думает, что данные на диске, хотя они еще в полете.4.5
cluster_client.cpp— infinite loop наcontinue_rw(CRITICAL)Если
handle_op_part()получает ошибку послеPART_SENT,continue_rw()никогда не сбрасываетPART_SENT. Происходит бесконечный цикл междуresume_1(пропускает failed part) иresume_2(решает, что не все done, и переходит обратно). Client event loop замирает.CACHE_REPEATINGbuffer не освобождается, запись теряется.4.6
cluster_client.cpp—continue_sync()лжет при пустомdirty_osds(CRITICAL)continue_sync()очищаетdirty_osds, отправляет SYNC, но если они все падают, при retrydirty_osdsуже пуст. Код делаетop->retval = 0; erase_op(op). Приложение считает данные durable, ни один OSD не вызвал fsync.4.7
cluster_client.cpp— fatal error write не удаляетCACHE_REPEATING(HIGH)При фатальной ошибке write
CACHE_REPEATINGbuffer остается вdirty_buffers. Последующий read возвращает данные failed write вместо данных с OSD.4.8
cluster_client.cpp—inc_wait()обходитretry_after(HIGH)inc_wait()вызываетcontinue_rw(next)без проверкиretry_after. Это может привести к реентерабельному вызовуcontinue_rw(), порчеop->state,inflight_count,done_count.4.9
cluster_client_wb.cpp— DELETE flush assert crash (HIGH)flush_buffers()формирует DELETE сlen > 0(merged). Callback делаетassert(op->retval == op->len). Если DELETE возвращает0, assert падает. Клиент крашится, все buffered writes теряются.5. Monitor / etcd (JavaScript)
5.1
mon/pool_config.js— ECparity_chunksvalidation bypass (CRITICAL)Math.floor(parity_chunks) || undefinedпревращает0иundefinedвundefined. Проверкаparity_chunks < 1даетfalseдляundefined. EC-пул без четности проходит валидацию. Кластер пишет объекты без parity. При отказе одного OSD — безвозвратная потеря данных.5.2
mon/antietcd_adapter.js— отсутствие CAS в мастер-выборах (CRITICAL)become_master()делает etcd txn безcompare. Два монитора одновременно могут считать себя master. Оба пишут конфликтующие PG maps. OSD получают split-brain конфигурацию и пишут в разные места.5.3
mon/mon.js+pg_gen.js— sparse PG array crash (HIGH)Если в etcd отсутствует середина диапазона PG keys, монитор вычисляет max key и считает, что
pg_countсовпадает. Затемpg_gen.jsобходитprev_pgs(sparse array) и падает сTypeError: Cannot read properties of undefined. Монитор в crash loop, конфигурация PG не обновляется.5.4
mon/stats.js—BigInt(float)exception (HIGH)(ratio * 1000000)— float.BigInt()кидает на дробных значениях. Монитор падает при каждом тике обновления stats. Нет master monitor для принятия решений о recovery/backfill.5.5
mon/osd_tree.js— пустое дерево по несуществующемуroot_node(HIGH)Если указан несуществующий root,
included = []. Все OSD удаляются изpool_tree. PG генерируются как[0,0,0]. Весь пул полностью недоступен.5.6
mon/mon.js— новые OSD не видны вstop_all_pgs(MEDIUM)all_osds()возвращает только OSD со статистикой. Новый OSD, зарегистрировавший state, но еще не написавший stats, не попадает в compare. Во время pause PG он может получить старую карту и начать принимать IO.5.7
mon/lp_optimizer.js—make_cyclicdead code (MEDIUM)for (const pg in pgs)— pg это string key, а не array.pg.sizeundefined, цикл не выполняется. EC placement optimization ломается, optimizer может упасть.5.8
mon/lp_optimizer.js—(1 << old_pg_size)32-bit overflow (MEDIUM)Для
old_pg_size >= 31битовый сдвиг оборачивается, optimizer берет неверную ветку. Перебалансировка становится некорректной.6. CLI / Disk Tools
6.1
disk_tool_prepare.cpp—force = true(CRITICAL)Многодисковый
prepare()жестко устанавливаетoptions["force"] = true. Проверка живых OSD/superblockов обходится. Возможна перезапись работающего OSD.6.2
disk_tool_meta.cpp— checksums пишутся во внешний bitmap (CRITICAL)В
write_json_heapвызовfromhexstr(..., wr->get_ext_bitmap(&heap))вместоwr->get_checksums(&heap). При импорте JSON metadata чекcумы затирают bitmap, а поле checksum остается uninitialized. OSD не сможет корректно проверять целостность.6.3
disk_tool_meta.cpp— infinite loop вindex_journal_by_object(CRITICAL)for (size_t i = jbo.size()-1; i >= 0; i--)—size_tunsigned. Приjbo.size() == 0подразумеваетсяSIZE_MAX, цикл бесконечен и OOB.6.4
disk_tool_resize.cpp—new_crc32_prevне обновляется (CRITICAL)После rebuild journal первый
crc32_prev= 0, а должен быть CRC новогоJE_START. При replay OSD считает весь journal corrupted. Все unflushed writes теряются.6.5
disk_tool_meta.cpp— деление на 0 приcsum_block_size == 0(CRITICAL)JSON metadata без явного
csum_block_size= 0. При расчете clean entry size происходит деление на 0. Tool падает с SIGFPE, диск в неопределенном состоянии.6.6
cli_modify.cpp— удаление данных до CAS (CRITICAL)При shrink
start_rm_data()вызывается до успешного etcd CAS. Если CAS падает (revision mismatch), данные уже удалены, а метаданные указывают на старый, больший размер. Образ полностью corrupted.6.7
cli_modify.cpp—--force-sizeбез--resize= truncate to zero (CRITICAL)Отсутствие
--resizeпри--force-sizeдаетnew_size=0. Вызывается полное удаление данных образа до 0 байт.6.8
cli_pool_rm.cpp— force parsing bug (CRITICAL)force = !cfg["force"].is_null()трактует присутствие ключа какtrue. Даже если"force": false, pool удаляется без проверок.6.9
disk_tool_prepare.cpp— отсутствие fsync (HIGH)write_zeroиwrite_osd_superblockне вызываютfsync/fdatasync. При power loss между prepare и первым OSD стартом superblock может не быть на диске. OSD перезаписывает старые данные.6.10
disk_tool_resize.cpp— unsigned underflownew_data_len(HIGH)Если
new_data_offset > dsk.data_device_size, вычислениеdsk.data_device_size - new_data_offsetоборачивается кUINT64_MAX. Resize работает с астрономическим размером.6.11
cli_fix.cpp—--check nobypass (HIGH)Пропускается проверка консистентности. Можно случайно удалить живые копии объектов.
6.12
cli_rm_data.cpp— read-only bypass (HIGH)OSD_OP_IGNORE_READONLYна каждом delete. Можно уничтожить read-only snapshots без подтверждения.7. NFS Gateway
7.1
nfs_kv_write.cpp— неверное число нулей в tail (CRITICAL)add_zero(op, st->offset, ...)вместо правильногоst->new_size - (st->offset + st->size). Хвост файла заполняется неправильным объемом (или мусором). Shared file corrupted.7.2
nfs_kv_write.cpp— игнорирование результата fsync (CRITICAL)nfs_do_fsynccallback не проверяетop->retval. Клиент получаетFILE_SYNC, хотя fsync OSD провалился. Данные не durable.7.3
nfs_kv_read.cpp— неинициализированный tail на short read (HIGH)При частичном read (
retval < len) оставшаяся часть буфера не обнуляется. NFS клиент получает мусор с кучи в tail.7.4
nfs_proxy.cpp— disconnect UAF (HIGH)nfs_client_t::refsучитывает только io_uring ops, не cluster ops. Disconnect при живых async ops вызываетdelete this. Callback пишет в освобожденную память.8. Block Frontends (ublk, NBD, QEMU)
8.1
ublk_server.cpp— отсутствие валидацииop->len(CRITICAL)iod->nr_sectors * 512напрямую превращается вop->lenбез сравнения сmax_io_buf_bytes. Kernel request может переполнить heap buffer.8.2
qemu_driver.c—qemu_aio_releaseна stack-allocated object (CRITICAL)Для QEMU < 2.8
task— stack variable.qemu_aio_releaseожидает heap. Memory corruption / double-free.8.3
nbd_proxy.cpp— отсутствие маскирования типа (HIGH)req_typeсравнивается через==безNBD_CMD_MASK_COMMAND. Upper flags вызываютexit(1). Все in-flight writes пропадают.8.4
nbd_proxy.cpp— отсутствие bounds check (HIGH)Нет проверки
offset+len <= device_size. OOB write может затронуть соседний объект.8.5
nbd_proxy.cpp— short read возвращает мусор (HIGH)При short read proxy отправляет
sizeof(nbd_reply) + op->len, хотя фактически вернулось меньше байт. Tail буфера неинициализирован.8.6
qemu_driver.c—BDRV_REQ_FUAигнорируется (HIGH)Guest journal/fs метаданные полагаются на FUA. Его отсутствие означает, что flush может не дойти до диска до kernel crash, что приводит к guest FS corruption.
8.7
qemu_driver.c— покрытие bitmap некорректно (HIGH)Проверка cache coverage идет до
offset+1вместоoffset+bytes. QEMU может пропустить выделенные блоки при convert/migration, что приводит к потере данных в целевом образе.8.8
qemu_driver.c— inverted readonly (MEDIUM)client->readonly = (flags & BDRV_O_RDWR) ? 1 : 0инвертирует логику. QEMU может отклонить легитимные writes.9. Utilities
9.1
ringloop.cpp— деструктор UAF (CRITICAL)~ring_loop_t()освобождаетring_datas/free_ring_dataдоio_uring_queue_exit. Если второй thread обрабатывает completion вloop(), он пишет в freed memory → UAF / double-free.9.2
ringloop.cpp— race вsubmit()(CRITICAL)submit()вызываетio_uring_submit()безmu. Multithread race на кольце — SQE могут перезаписываться / теряться. Write operations исчезают из SQE.9.3
ringloop.cpp— race вrestore()(CRITICAL)restore()возвращает slots в freelist без блокировки. Конкуренция сsubmit()/loop()дает double-free и выдачу одного slot двум операциям → callback mix-up.9.4
crc32c.c—crc32c_nopadигнорируетprev_crc(HIGH)API promise incremental CRC, но реализация всегда использует seed 0. Любой future caller сломает checksum consistency, маскируя corruption.
9.5
timerfd_manager.cpp— overflow millis*1000 (HIGH)set_timerперемножает без проверки. Огромный таймаут lease/peering оборачивается к 0, таймер стреляет мгновенно. Split-brain и потеря safety properties.9.6
timerfd_manager.cpp— exception в callback замораживает все таймеры (HIGH)onstackне сбрасывается при exception. Все последующиеset_nearest()возвращаются рано. Peering timeouts, flush timers, heartbeats перестают срабатывать.9.7
allocator.cpp— bitmap truncation (MEDIUM)Смещения приводятся к
unsigned(32-bit). Диски с bitmap >4 GiB — truncation. Allocator помечает/чистит неправильные биты. Повреждение блоков.Вердикт / Классификация угроз для облачного IaaS
Приоритеты исправления (Recommended Fix Priority)
Tier 1 — Немедленный патч перед продакшеном
fsync_batchв v1 (ждать state==2), добавить финальныйfsync_metaв v3 flush, починить in-place compaction (COW или invalidate old entry atomically).pg_minsize == 1обработку вcalc_rmw_parity_xorиreconstruct_stripes_xor(special-case или memcpy).PG_PEERINGпосле завершения; очищать maps вcalc_object_states.compareвbecome_masterи все etcd writes; починитьpool_config.jsвалидацию EC.len/attr_len/json_lenверхними границами; добавить проверку на integer overflow вsec_rw.len + attr_len.copy_write; добавить flush + дождаться завершения в деструкторе.Tier 2 — В течение 1-2 недель
osd_numв stripes для replicated reads; не пропускать rollback для offline peers (queue recovery вместо silent skip).je->size >= min_size_for_type; обработать wrap small-write данных корректно.force=true, добавить fsync перед close, исправить unsigned underflow в resize.op->retvalfsync, исправить tail zero fill, обрабатывать partial read как ошибку.Tier 3 — В течение месяца
std::mutexнаmatrices; защитить ringloop сmuво всех операциях; добавить RAII дляonstack.blockstore_disk.cpp; использоватьsizeof()вместо hardcoded 24.Заключение
Vitastor — это сложная распределенная система хранения, и в ней обнаружено значительное количество критических уязвимостей, способных привести к потере, порче или утечке данных клиентов в публичном облаке. Особенно опасны баги в blockstore flush/fsync ordering, OSD peering state machine, EC parity calculation, messenger length validation, client writeback accounting, и monitor etcd consistency.
Отчет сформирован автоматически в результате глубокого параллельного аудита ~25 подсистем Vitastor с фокусом на data-path, metadata-path, recovery, networking и consistency.
Короче тут потенциально полезного только:
Остальное точно неактуально
Кстати нумерация в таблице и тексте по ходу не совпадает
Ну закрывать не обязательно, пару пунктов надо таки проверить ещё)