Data integrity review using AI (gpt-5.5) #66
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-11
Ревизия:
caa70317faab41f92b0fd017f4437da5b2386a40Фокус: сценарии, в которых production-кластер может подтвердить запись/delete/sync, а затем потерять данные, воскресить удаленные данные или закрепить неверный EC-набор как правильный.
Это read-only аудит кода. Я не запускал разрушительные тесты на реальном кластере. Ниже перечислены только находки, которые выглядят релевантными для durability/data-loss/data-corruption. Часть пунктов требует минимального воспроизводящего теста, но несколько нарушений подтверждаются прямо по потоку кода.
Краткое резюме
Самые опасные места:
BS_OP_SYNCв blockstore может вернуть успех второму sync до завершения фактическогоfsync.OSD_OP_SYNCочищаетdirty_osdsдо результата и при retryable ошибке может повторно завершиться успешно без повторного sync.data_fd, но не учитывает их вunsynced_data_write_count; metadata может стать durable раньше данных.meta_format=3delete хранится как stable tombstone версии0, что ломает версионную семантику delete/read/rollback иpg.ver_override.DATA-001:
BS_OP_SYNCможет завершиться до фактическогоfsyncСерьезность: Critical
Уверенность: высокая
Компонент:
src/blockstoreКод:
src/blockstore/blockstore_sync.cpp:29-87src/blockstore/blockstore_sync.cpp:101-108src/blockstore/blockstore_impl.cpp:138-142Проблема:
submit_fsyncs()сбрасываетunsynced_data_write_count,unsynced_buffer_write_countиunsynced_meta_write_countсразу после отправкиio_uring_prep_fsync, до completion callback:src/blockstore/blockstore_sync.cpp:84-86После этого второй
BS_OP_SYNC, обработанный в той же очереди до completion первого fsync, видитhas_unsynced() == falseнаsrc/blockstore/blockstore_sync.cpp:101и возвращает успех немедленно.Почему это может привести к потере данных:
unsynced_*счетчиков.BS_OP_SYNCотправил fsync и тут же обнулил счетчики.BS_OP_SYNCувидел пустое dirty-состояние и завершился успешно.Это нарушает базовую семантику
fsync: подтвержденный sync обязан переживать crash.Идея исправления:
unsynced_*счетчики до completion всех отправленных fsync.Regression-test:
io_uring_prep_fsync, чтобы задержать completion.DATA-002: client
continue_sync()теряетdirty_osdsпри retryable ошибкеСерьезность: Critical
Уверенность: высокая
Компонент:
src/clientКод:
src/client/cluster_client.cpp:1463-1527Проблема:
cluster_client_t::continue_sync()очищаетdirty_osdsсразу после отправки sync-подопераций:src/client/cluster_client.cpp:1485-1501Если один из OSD вернул retryable
-EPIPE,-EIOили-ENOSPC, код вызываетwb->fsync_error(), сбрасывает состояние операции и возвращает retry:src/client/cluster_client.cpp:1508-1519Но список
dirty_osdsуже потерян. На следующем заходеcontinue_sync()попадает в fast path:src/client/cluster_client.cpp:1467-1472и завершает fsync успешно без повторной отправки
OSD_OP_SYNC.Почему это может привести к потере данных:
dirty_osds.clear().Идея исправления:
dirty_osdsтолько после успеха, причем аккуратно: не удалять OSD, которые стали dirty новыми writes во время inflight sync.Regression-test:
-EIO/-ENOSPCс retry.dirty_osds.DATA-003: delete на misplaced/extra OSD может не попасть в sync set и затем воскреснуть
Серьезность: Critical
Уверенность: высокая
Компонент:
src/osdКод:
src/osd/osd_primary.cpp:698-710src/osd/osd_primary.cpp:766-797src/osd/osd_primary_subops.cpp:545-570src/blockstore/blockstore_heap.cpp:2175-2227Проблема:
Delete отправляется во все chunks из
object_state->osd_set, если объект находится не только в текущем clean set:src/osd/osd_primary.cpp:769-770src/osd/osd_primary_subops.cpp:545-570После успешных delete subops код удаляет объект из
object_stateи вызываетderef_object_state():src/osd/osd_primary.cpp:790-791Если это был последний объект в данном
state_dictentry,deref_object_state()стирает state и зануляетop_data->object_state:src/osd/osd_primary.cpp:706-709Только после этого код строит dirty OSD set:
src/osd/osd_primary.cpp:793-797Если
op_data->object_stateуже сталNULL, цикл беретpg.cur_loc_set, а не фактический set, куда были отправлены delete. Extra/misplaced OSD, напримерX, теряется из dirty set.Почему это может привести к resurrection старых данных:
Xвнеpg.cur_loc_set.DELETEотправлен и на current set, и наX.Xвернул success, но приimmediate_commit=noneэто еще не означает durable fsync.Xне добавлен вdirty_osds, потому чтоobject_stateбыл освобожден до построения dirty set.Xпадает/рестартует до durable delete.Xснова попадает в list. Так как stable delete версии0не возвращается как нормальная stable version вlist_objects()(src/blockstore/blockstore_heap.cpp:2210-2227), старая копия может быть воспринята как recoverable/misplaced данные.Идея исправления:
remove_object_from_state()сохранить точныйloc_set, куда реально отправлялся delete.object_state.Regression-test:
immediate_commit=none.X.Xдо fsync его delete.DATA-004: compaction пишет data blocks без учета в
unsynced_data_write_countСерьезность: Critical/High
Уверенность: высокая
Компонент:
src/blockstoreКод:
src/blockstore/blockstore_flush.cpp:380-395src/blockstore/blockstore_flush.cpp:421-432src/blockstore/blockstore_sync.cpp:21-33src/blockstore/blockstore_write.cpp:428-449Проблема:
Во время compaction flusher читает small writes из journal и пишет объединенные данные в
data_fd:src/blockstore/blockstore_flush.cpp:380-395Затем добавляет metadata entry через
add_compact()и пишет metadata block:src/blockstore/blockstore_flush.cpp:421-432Но эти data writes нигде не увеличивают
unsynced_data_write_count. Поиск всех мест изменения счетчиков показывает, что data counter увеличивается только в обычном write path (src/blockstore/blockstore_write.cpp:428-449), а flusher data writes не учитываются.Почему это может привести к silent corruption:
unsynced_meta_write_countи fsync-ит metadata, но не data device, особенно при раздельныхdata_device/meta_device.Если checksums включены, это может проявиться как checksum error; без checksums это может быть тихое чтение старых байтов.
Идея исправления:
data_fdдолжны участвовать в той же durability accounting, что и normal big writes.Regression-test:
data_deviceиmeta_device.DATA-005:
meta_format=3delete хранится как stable version0и ломает версионностьСерьезность: High/Critical
Уверенность: высокая
Компонент:
src/blockstore, затрагиваетsrc/osdКод:
src/blockstore/blockstore.h:67-94src/blockstore/blockstore_write.cpp:125-141src/blockstore/blockstore_heap.cpp:1867-1871src/blockstore/blockstore_read.cpp:36-40src/blockstore/blockstore_read.cpp:455-495src/osd/osd_primary.cpp:766-770src/osd/osd_primary_chain.cpp:86-96src/osd/osd_primary_chain.cpp:164-166Проблема:
Blockstore API документирует:
version == 0означает auto-assign новой версии;versionтрактуется как у write, delete "initially written as an object version".Но v3/heap path для delete всегда вызывает:
src/blockstore/blockstore_heap.cpp:1867-1871и записывает
BS_HEAP_DELETE|BS_HEAP_STABLEсversion = 0, игнорируяop->version.Это конфликтует с несколькими инвариантами:
0используется как sentinel для отсутствия stable version (src/blockstore/blockstore_heap.cpp:2210-2227).read_bitmap()и обычный read останавливаются на delete. Так как delete имеет version0, любойtarget_version >= 0попадает в tombstone и не может прочитать предыдущую версию (src/blockstore/blockstore_read.cpp:36-40,src/blockstore/blockstore_read.cpp:463-468).pg.ver_override[oid] = old_versionперед отправкой delete, чтобы параллельные reads не увидели незавершенный delete (src/osd/osd_primary.cpp:766-770), но blockstore delete version0делает такой override неэффективным на OSD, где tombstone уже применился.Отдельный явный баг в chained reads:
Fast path для clean replicated PG читает bitmaps локально с
UINT64_MAX:src/osd/osd_primary_chain.cpp:86-96а slow path учитывает
pg.ver_override:src/osd/osd_primary_chain.cpp:164-166То есть snapshot/chained read может увидеть незавершенный delete даже на уровне bitmap selection.
Почему это может привести к повреждению/потере данных:
pg.ver_overrideна старую версию.0.Идея исправления:
op->version, а не как stable sentinel0, либо явно перепроектировать все readers/list/rollback/ver_override вокруг sentinel delete.read/read_bitmapдолжны уметь читать версию до delete, еслиtarget_version < delete_version.osd_primary_chain.cppfast path должен использоватьpg.ver_override, как slow path.Regression-test:
DATA-006:
version == 0для write вmeta_format=3не auto-assignитсяСерьезность: High для blockstore API, exposure зависит от callers
Уверенность: высокая
Компонент:
src/blockstoreКод:
src/blockstore/blockstore.h:67-69src/blockstore/blockstore_write.cpp:148-151src/blockstore/blockstore_write.cpp:223-251src/blockstore/blockstore_write.cpp:365-366src/blockstore/blockstore_heap.cpp:1554-1601src/blockstore/blockstore_heap.cpp:376-380src/blockstore/v1/write.cpp:79-82Проблема:
API говорит, что write с
version == 0получает новую версию автоматически. В v1 path это действительно есть:src/blockstore/v1/write.cpp:79-82В heap/v3 path
op->versionнапрямую передается вadd_small_write()/add_big_write()и затем в metadata:src/blockstore/blockstore_write.cpp:223-251src/blockstore/blockstore_write.cpp:365-366src/blockstore/blockstore_heap.cpp:1571src/blockstore/blockstore_heap.cpp:1600Таким образом, публично допустимый write может попасть на диск как object version
0.Почему это опасно:
0не возвращаетсяlist_objects(), потому чтоstable_version == 0означает "нет stable object".STABLEдля такой записи может породитьBS_HEAP_COMMITversion0, а loader считает commit version0повреждением metadata:src/blockstore/blockstore_heap.cpp:376-380Exposure:
OSD primary обычно назначает nonzero версии сам. Но
fio_engine,fio_sec_osd, tests и потенциальные direct blockstore users полагаются наversion=0auto-assign. Посколькуmeta_format=3является heap format и выставляется по умолчанию в prepare path (src/disk_tool/disk_tool_prepare.cpp:126-128,src/blockstore/ondisk_formats.h:149-151), это не выглядит как legacy-only проблема.Идея исправления:
0в heap metadata для normal write/commit.0.Regression-test:
meta_format=3.BS_OP_WRITEилиBS_OP_WRITE_STABLEсversion=0.DATA-007: EC scrub
ec_find_good()может выбрать неверный good subset или выйти за границыСерьезность: High
Уверенность: высокая
Компонент:
src/osdEC/XOR scrub/recoveryКод:
src/osd/osd_scrub.cpp:372-425src/osd/osd_scrub.cpp:542-546src/osd/osd_rmw.cpp:1277-1400src/osd/osd_rmw_test.cpp:1140и далееПроблема 1: неверная инициализация
eq_toec_find_good()создаетint eq_to[stripe_count], но инициализирует цикл доpg_size:src/osd/osd_rmw.cpp:1282-1287А scrub для unclean object строит
stripe_countизobject_state->osd_set, исключая outdated chunks:src/osd/osd_scrub.cpp:395-413В результате
stripe_countможет быть меньше или большеpg_size.stripe_count < pg_size: stack out-of-bounds write.stripe_count > pg_size: частьeq_to[]остается неинициализированной, а дальше читается наsrc/osd/osd_rmw.cpp:1308.Проблема 2: временный буфер выделен по
stripe_count, а используется поpg_sizetmp_bufвыделяется как:src/osd/osd_rmw.cpp:1340но
ec_check_combination()вызывается сpg_sizeи может индексировать временные data/bitmap buffers по ролям доpg_size. Для degraded EC, например 5+3, где scrub видит 6 или 7 live chunks приpg_size=8, это может дать heap out-of-bounds.Проблема 3: перебор duplicate role variants использует неправильный индекс
Чтение subset делает правильное отображение role ordinal -> real role:
src/osd/osd_rmw.cpp:1353Но счетчик subvariant проверяется так:
src/osd/osd_rmw.cpp:1384То есть используется
live_variants[combination[i]], а неlive_variants[comb_to_subset[combination[i]]].Для sparse roles, например
{2,4,5}или при duplicate role после rebalance/recovery, перебор может пропустить правильный variant или остановиться по размеру другого role.Почему это может привести к data loss/corruption:
Scrub с
scrub_find_bestможет:После этого recovery/fix может переписать missing/outdated chunks уже на основе неверного
good_subset.Идея исправления:
ec_find_good()должны иметь четкую модель: indexed by dense stripe index или by EC role. Сейчас эти модели смешаны.eq_toинициализировать поstripe_count.pg_size, если downstream indexing идет по role/pg index.comb_to_subset.stripes[i].role < pg_size.Regression-tests:
pg_size=8,pg_minsize=5,stripe_count=6/7, nonzero bitmap, missing high role; запуск под ASAN.stripe_count > pg_size, duplicate role, missing role 0: первый duplicate variant битый, второй корректный. Проверить, что выбран корректный variant.eq_to.DATA-008: scrub marking использует
chunk.roleкак индекс densestripes[]Серьезность: High/Medium-High
Уверенность: средне-высокая
Компонент:
src/osdscrub state markingКод:
src/osd/osd_scrub.cpp:599-646Проблема:
После scrub code ищет в
new_setтот же chunk через dense indexi:src/osd/osd_scrub.cpp:603-622Но затем для очистки
LOC_CORRUPTEDиспользует:src/osd/osd_scrub.cpp:628-632op_data->stripes[chunk.role]корректно только еслиstripes[]индексирован по role. Для unclean objectsubmit_scrub_subops()строитstripes[]как плотный список non-outdated chunks изobject_state->osd_set:src/osd/osd_scrub.cpp:395-413Там index != role при EC degraded/misplaced/duplicate/sparse roles.
Почему это может привести к повреждению:
missingот другого chunk.chunk.role >= stripe_count.LOC_CORRUPTEDс bad chunk.Следующий recovery будет использовать состояние объекта, где corrupted/missing flags не соответствуют фактическим данным.
Идея исправления:
iдля состоянияop_data->stripes[i].role -> dense indexmap с учетом duplicates, или явно запретить ambiguous lookup.Regression-test:
{1,2,4,5}или duplicate role.stripes[role].LOC_CORRUPTEDочищается/ставится только на фактически проверенный(osd_num, role).DATA-009: writeback
CACHE_REPEATINGможет не replay-иться при смене primaryСерьезность: High/Medium-High
Уверенность: средняя, зависит от гарантий старого primary/connection teardown
Компонент:
src/clientwriteback/lazy writeКод:
src/client/cluster_client.cpp:613-620src/client/cluster_client.cpp:794-800src/client/cluster_client.cpp:1167-1170src/client/cluster_client_wb.cpp:260-291src/client/cluster_client_wb.cpp:294-321Проблема:
При смене primary клиент вызывает:
src/client/cluster_client.cpp:613-620repeat_ops_for()должен повторить операции PG, потому что OSD, переставший быть primary, мог не fsync-нуть операции. Но он исключает buffers в состоянияхCACHE_REPEATINGиCACHE_DIRTY:src/client/cluster_client_wb.cpp:270Обычная non-immediate write перед отправкой сохраняется как
CACHE_REPEATING:src/client/cluster_client.cpp:794-800После success callback клиент помечает buffer как written:
src/client/cluster_client.cpp:1167-1170Сценарий риска:
CACHE_REPEATING.repeat_ops_for()не replay-ит этот buffer из-за фильтраCACHE_REPEATING.CACHE_WRITTEN, хотя новый authoritative primary мог не получить эту запись.Если протокол всегда гарантированно рвет такие операции при смене primary и никогда не возвращает stale success, риск снимается. Но на стороне клиента эта гарантия явно не проверяется.
Идея исправления:
CACHE_REPEATINGдля затронутой PG потенциально obsolete и повторять/invalidating их.Regression-test:
DATA-010: writeback
copy_write()копирует iovec без ограниченияop->lenСерьезность: Medium-High
Уверенность: высокая для memory corruption при некорректном iov, production exposure зависит от callers
Компонент:
src/clientКод:
src/client/cluster_client_wb.cpp:247-256src/client/vitastor_c.cpp:316Проблема:
copy_write()выделяет buffer подop->len, но копирует каждый iovec полнымiov.iov_len:src/client/cluster_client_wb.cpp:247-256Переменная
lenинициализируется, но не уменьшается. Нет проверки, что сумма iovec равнаop->len.Почему это опасно:
sum(iov_len) > op->len: heap overwrite за пределы writeback buffer.sum(iov_len) < op->len: хвост buffer остается неинициализированным и может быть позже записан как stale data.Если все internal callers всегда формируют iovec строго под
op->len, production exposure ниже. Но public C API прокидывает caller iov без очевидной нормализации суммы.Идея исправления:
len.sum(iov_len) == op->len, иначе возвращать-EINVAL.Regression-test:
len=4096, iov sum 8192: ожидать-EINVAL, не overwrite.len=4096, iov sum 2048: ожидать-EINVAL, не запись мусора.DATA-011: messenger не сверяет reply opcode с исходным request opcode
Серьезность: Medium
Уверенность: высокая по коду, production exposure в основном при bug/corrupt/malicious peer
Компонент:
src/clientmessengerКод:
src/client/msgr_receive.cpp:348-394src/client/msgr_receive.cpp:395-434Проблема:
Reply сопоставляется с pending request только по
id:src/client/msgr_receive.cpp:350Затем header копируется в original op:
src/client/msgr_receive.cpp:359Но нет проверки, что
op->reply.hdr.opcode == op->req.hdr.opcode. Далее receive path доверяет reply opcode, чтобы решить, надо ли читать payload:src/client/msgr_receive.cpp:361-394Риск:
Wrong/corrupt peer reply с корректным id, но другим opcode может:
Это больше protocol robustness/security finding, но для storage-системы лучше fail-stop, чем silent wrong reply handling.
Идея исправления:
DATA-012: read error reply может overflow-нуть bitmap buffer
Серьезность: Medium
Уверенность: высокая по коду, production exposure в основном при bug/corrupt/malicious peer
Компонент:
src/clientmessengerКод:
src/client/msgr_receive.cpp:361-380Проблема:
Проверка
bmp_len > op->bitmap_lenвыполняется только еслиretval >= 0:src/client/msgr_receive.cpp:366-374Но после этого код все равно добавляет
bmp_lenbytes в receive list, даже приretval < 0:src/client/msgr_receive.cpp:375-380Bad/corrupt error reply для
OSD_OP_READилиOSD_OP_SEC_READс nonzerobitmap_len/attr_lenможет записать за пределы bitmap/RMW buffer.Идея исправления:
bmp_len <= op->bitmap_lenнезависимо отretval.DATA-013: wire lengths/counts приводятся к
int read_remainingСерьезность: Medium
Уверенность: высокая по коду, production exposure зависит от max object list sizes и trust peer
Компонент:
src/clientmessenger/listingКод:
src/client/messenger.h:79src/client/msgr_receive.cpp:395-434src/client/cluster_client_list.cpp:409Проблема:
read_remainingимеет типint, а некоторые размеры приходят какint64_t/uint64_tили вычисляются из count:src/client/msgr_receive.cpp:401src/client/msgr_receive.cpp:411src/client/msgr_receive.cpp:421src/client/msgr_receive.cpp:431Например
sizeof(obj_ver_id) * retvalдляOSD_OP_SEC_LISTможет переполнить/truncateint, после чего consumer все еще может идти по полномуretval.Риск:
На очень больших листингах или corrupt reply можно получить partial receive, stream desync и out-of-bounds consumption. Если это попадет в repair/remove/list workflow, это может привести к неправильным решениям верхнего уровня.
Идея исправления:
read_remainingдолжен бытьuint64_t/size_tс проверкой верхней границы.retval <= max_objects_per_replyиpayload_size <= configured_max.DATA-014: peering может индексировать
cur_setinvalid role у outdated chunkСерьезность: Medium
Уверенность: средняя
Компонент:
src/osdpeeringКод:
src/osd/osd_peering_pg.cpp:289-309Проблема:
Для outdated versions после выбранного target version код делает:
src/osd/osd_peering_pg.cpp:302без повторной проверки, что
(list[i].oid.stripe & STRIPE_MASK) < pg->pg_size.Invalid role chunks проверяются раньше, пока выбирается target version, но stale/invalid chunk может встретиться только в более старой версии.
Риск:
Это больше похоже на crash/peering robustness bug, чем на прямую silent corruption. Но при поврежденных/stale replicas peering должен fail-stop с
PG_HAS_INVALID, а не читать за пределамиcur_set.Идея исправления:
cur_setпроверять range.PG_HAS_INVALID/safe error path.Рекомендованный порядок triage
BS_OP_SYNCaccounting (DATA-001) и client sync retry (DATA-002). Это прямые acknowledged-durability нарушения.DATA-003,DATA-005), потому что они затрагивают удаление и peering.DATA-004), особенно если используются раздельные data/meta устройства.DATA-007,DATA-008), особенно если включены EC/XOR pools и scrub/fix.DATA-009,DATA-010).DATA-011..DATA-014) как hardening, желательно с fuzz/property tests.Общие рекомендации по тестам
data_device,meta_device,journal_device, потому что часть багов маскируется общим fd.ec_find_good()на случайные sparse roles, duplicate roles, missing roles и random corruption.Посмотрел, тут резюме:
Интересно, что почти не пересекается с отчётом клода :)
Вообще все тесты гоняются под asan
Апдейт:
Тут вроде всё