Data integrity audit using AI (Claude Fable 5 + /effort max + workflow) #75

Open
opened 2026-06-09 23:53:00 +03:00 by maxpain · 5 comments

Аудит Vitastor: баги, способные привести к повреждению или потере данных

Версия: commit c16f955a (3.0.13+2), чистое рабочее дерево.
Область: весь C/C++ код (src/, node-binding/), кроме вендоренного кода (json11, cpp-btree, src/liburing, src/util/robin_hood.h) и rust-крейта. JS-монитор (mon/) не рассматривался.
Методология: код прочитан целиком пофайлово; каждый кандидат проверялся несколькими независимыми «адверсариальными» проходами, плюс ручная перепроверка по исходникам всех критических и части высоких находок. Отсев ложных срабатываний — приоритет.

Помечено [проверено вручную] — то, что я лично подтвердил по коду. Помечено [требует подтверждения мейнтейнера] — реальный по механике дефект, но триггер узкий/сложный и я не смог довести трассу до 100%.


? Критические (тихая потеря/порча зафиксированных данных)

1. vitastor-cli modify --resize <меньший размер> удаляет ВСЕ данные образа, а не «хвост» [проверено вручную]

Файл: src/cmd/cli_modify.cpp:110-116 → src/cmd/cli_rm_data.cpp:331-334

При сжатии образа image_changer_t::loop вызывает start_rm_data, передавая ключи через дефис:

cb = parent->start_rm_data(json11::Json::object {
    ...
    { "min-offset", ((new_size+4095)/4096)*4096 },   // дефис!
    { "down-ok", down_ok },
});

А start_rm_data читает ключи через подчёркивание: cfg["min_offset"] (строка 333), cfg["down_ok"] (331). Нормализация -→_ (cli_tool_t::parse_config, cli_common.cpp:104-116) применяется только к argv верхнего уровня в cli.cpp:331 и не применяется к этому внутреннему JSON. В результате min_offset отсутствует → json11 возвращает null → uint64_value() == 0.

При min_offset==0 && max_offset==0 код rm_inode_t считает это «без границы» и удаляет все объекты инода (cli_rm_data.cpp:70-73 и условие на :97 stripe >= 0 && (!0 || …) всегда истинно). После этого cfg.size уменьшается, транзакция в etcd коммитится, печатается «Image … modified».

Триггер: документированная операция — vitastor-cli modify myimage --resize 500G на образе 1 ТБ после уменьшения ФС внутри. Также достижимо через vitastor-nfs block-mode SETATTR-truncate (nfs_block.cpp:192-199) и vitastor-cli dd conv=trunc на существующий бо́льший образ.
Последствие: полная тихая потеря всех данных образа при «безопасной» операции сжатия; команда рапортует успех.
Фикс: передавать min_offset/down_ok (подчёркивание) — как это уже корректно делает nfs_kv_setattr.cpp:133.

Тот же «мёртвый ключ» есть в cli_rm.cpp:680-681 (down-ok/fsync-interval), но там min-offset не передаётся (удаляется весь инод по замыслу), поэтому игнорируется лишь --down-ok — это «fail-safe», не потеря данных.

2. Удалённое неаутентифицированное переполнение кучи в OSD через OSD_OP_SHOW_CONFIG [проверено вручную]

Файл: src/client/msgr_receive.cpp:318-327, handle_op_hdr

else if (cur_op->req.hdr.opcode == OSD_OP_SHOW_CONFIG) {
    if (cur_op->req.show_conf.json_len > 0) {
        cur_op->buf = malloc_or_die(cur_op->req.show_conf.json_len+1);   // json_len = UINT64_MAX → malloc(0)
        ((uint8_t*)cur_op->buf)[cur_op->req.show_conf.json_len] = 0;     // запись по buf[-1]
        cl->recv_list.push_back(cur_op->buf, cur_op->req.show_conf.json_len); // iovec длиной 2^64-1
    }
    cl->read_remaining = cur_op->req.show_conf.json_len;                 // int ← uint64: -1
}

json_len — 64-битное поле с провода (osd_ops.h:183), не валидируется. При json_len = 0xFFFFFFFFFFFFFFFF: json_len+1 переполняется в 0 → malloc_or_die(0) отдаёт крошечный буфер; в recv_list кладётся iovec длиной 2^64-1. read_remaining — int (messenger.h:79), становится -1, поэтому op помечается «готовым» через ветку else, не сбрасывая recv_list (он был сброшен в handle_finished_read:217 до вызова). На следующей итерации handle_read_buffer остаток принятых байт (до ~64 КБ за один recv) memcpy-ится в этот гигантский iovec поверх malloc(0)-буфера (msgr_receive.cpp:185).

Проверка магии в handle_finished_read:222 (SECONDARY_OSD_OP_MAGIC) не требует аутентификации; accept_connections принимает любого TCP-пира. В вашей модели угроз (tenant-VM имеют сетевой доступ к data-портам OSD через QEMU/NBD-драйвер) это критично.
Последствие: удалённое переполнение кучи на единственном event-loop OSD → краш/DoS, порча соседних буферов (в т.ч. данных конкурентных записей до сброса на диск), потенциально RCE.
Фикс: валидировать json_len (и вообще все длины с провода) разумным верхним пределом до аллокации.

3. ublk в режиме образа (--image, дефолт и дефолт CSI): fsync гостя не доходит до OSD [проверено вручную]

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

const bool writeback = !cli->get_immediate_commit(inode);   // inode == 0 в режиме образа!
auto pool_it = cli->st_cli.pool_config.find(INODE_POOL(inode ? inode : watch->cfg.num));  // ← правильный fallback строкой ниже

В режиме образа inode всегда 0 (ublk_server.cpp:196-201), реальный инод — в watch->cfg.num. get_immediate_commit(0) из-за раннего if (!pool_id) return true; (cluster_client.cpp:631-632) возвращает true → writeback=false → в add_device не выставляется UBLK_ATTR_VOLATILE_CACHE (ublk_server.cpp:318). Ядро считает устройство write-through и не шлёт FLUSH, поэтому обработчик UBLK_IO_OP_FLUSH → OSD_OP_SYNC (ublk_server.cpp:772-775) никогда не вызывается. То, что соседняя строка 286 уже использует fallback inode ? inode : watch->cfg.num, доказывает: автор знал про inode==0 и забыл применить fallback в строке 285 (предыдущий фикс 85c7e3bd чинил полярность этой строки, но пропустил случай образа).

Триггер: пул с immediate_commit=none/small (документированная норма для HDD и SSD без конденсаторов) + том примонтирован vitastor-ublk map --image … (или через Kubernetes CSI, где ublk теперь дефолт). Гость делает fsync() → возвращает успех, данные не синкнуты на OSD → потеря питания/краш OSD → подтверждённые fsync-ом данные теряются.
Последствие: тихая потеря данных, подтверждённых fsync(), на любом пуле кроме immediate_commit=all; повреждение журналов гостевых ФС.
Фикс: cli->get_immediate_commit(inode ? inode : watch->cfg.num).

4. vitastor-disk resize тихо рушит метаданные heap-OSD при области метаданных > 4 ГиБ [проверено вручную]

Файл: src/disk_tool/disk_tool_resize.cpp:565,594,598,616 + src/blockstore/blockstore_heap.h:364

new_meta_pos — uint64_t (строка 565), но blockstore_heap_t::fill_block_empty_space(uint8_t*, uint32_t pos) принимает uint32_t. На вызовах 594/598/616 позиция ≥ 4 ГиБ тихо усекается по модулю 2³². Внутри (blockstore_heap.cpp:2367-2384) функция пишет 4-байтовый маркер конца блока {size, BS_HEAP_FREE_SPACE} по buffer + (pos % meta_block_size), продвинув buffer на усечённый индекс блока. То есть маркер «дальше блок свободен» для позиции ≥ 2³² попадает в низкий блок метаданных, где уже лежат реальные записи (а при ровно 2³² — в суперблок). Копии самих записей (memcpy на строке 624) используют корректную 64-битную арифметику, поэтому портятся именно маркеры — загрузчик молча считает заполненные низкие блоки пустыми.

Триггер: heap-формат (Vitastor 3.0), meta_area_size > 4 ГиБ — штатно для OSD ≥ 4 ТБ со 128К блоками и crc32c (min_meta_len ~5.8 ГиБ, а vitastor-disk prepare резервирует 2× минимума). Админ запускает vitastor-disk resize/raw-resize.
Последствие: массовая тихая потеря зафиксированных объектов на resized-OSD (часть блоков загрузчик видит пустыми, без ошибки; либо «Metadata is corrupt»).
Фикс: объявить pos как uint64_t/size_t в fill_block_empty_space.

5. Повторная отправка устаревшего writeback-флаша перезаписывает уже подтверждённые fsync-ом данные [требует подтверждения мейнтейнера]

Файл: src/client/cluster_client_wb.cpp:300-364, src/client/cluster_client.cpp:196-211

Подтверждённая механика: в calc_wait два флаш-op (OP_FLUSH_BUFFER) не упорядочиваются друг относительно друга — условие на строке 204 требует !(op->flags & OP_FLUSH_BUFFER). При сбое флаша F1 (рестарт OSD, -EPIPE) он паркуется на client_retry_interval (cluster_client.cpp:1600-1606), при этом захваченный им буфер со старыми данными жив (refcnt в flushed_buffers, op->iov указывает на него). Свежий перекрывающий флаш F2 имеет retry_after=0 и отправляется первым; после его коммита запоздавший F1 повторно пишет старые данные тем же диапазоном без версии/CAS.

Триггер (узкое окно): client_enable_writeback=true (QEMU cache=writeback, NBD, ublk, NFS); флаш по лимиту буфера без хвостового SYNC; рестарт primary OSD; перезапись того же горячего блока + fsync в окне ретрая.
Последствие: старая версия блока становится durable после успешного fsync новой версии (например, commit-блок журнала гостевой ФС) → тихая порча, видимая при последующих чтениях.
Фикс: упорядочить повторную отправку флаша относительно более новых перекрывающихся флашей того же диапазона.


? Высокие

Хранилище / OSD

  • src/blockstore/blockstore_disk.cpp:543-556 — trim_data дискардит ЖИВЫЕ блоки [проверено вручную]. После выравнивания по discard_granularity пустой диапазон делает continue, пропуская j = i+1 (строка 556). j остаётся на старом начале пробега, и следующий BLKDISCARD накрывает промежуточный используемый блок → физическое уничтожение зафиксированных данных (на чтении — нули; с checksum — EIO). Достижимо через vitastor-disk trim и discard_on_start. Фикс: продвигать j даже при пустом выровненном диапазоне.

  • src/osd/osd_rmw.cpp:1211-1218 — переполнение кучи в скрабе degraded EC-объекта. ec_check_combination индексирует tmp_buf по роли [0, pg_size), но ec_find_good (:1341) выделяет tmp_buf размером stripe_count*(chunk_size+bitmap_size), где stripe_count = число присутствующих чанков < pg_size. Для EC с ≥2 parity и объектом, потерявшим 1..(parity-1) чанков, восстановление пишет chunk_size (≥128К) байт за пределы аллокации. Последствие: краш OSD при скрабе любого degraded EC-объекта; возможна тихая порча соседней кучи.

  • src/osd/osd_scrub.cpp:630-635 — скраб снимает LOC_CORRUPTED с всё ещё битых чанков. Массив stripes индексируется по chunk.role вместо i, плюс нет проверки !read_error → для EC ещё и OOB-чтение. Последствие: скраб может пометить реально повреждённый чанк как хороший / неверно «починить».

  • src/osd/osd_primary_chain.cpp:131-148 — неверная реконструкция bitmap при цепочечном (snapshot) чтении EC. В качестве источников декодирования используются никогда не запрошенные нулевые bitmap недоступных/незагруженных чанков → неверная карта присутствия данных → неверные результаты чтения снапшот-цепочек.

  • src/blockstore/blockstore_sync.cpp:30-88 — sync может быть подтверждён без нужного fsync [требует подтверждения мейнтейнера]. Дедупликация fsync по совпадению fd: при раскладке data_fd == journal_fd != meta_fd и наличии незасинканных data-записей без buffer-записей ни data-, ни journal-fsync не выполняется (data-fsync пропущен по data_fd==journal_fd, journal-fsync — по unsynced_buffer_write_count==0). Последствие: в нестандартной раскладке дисков метаданные становятся durable, ссылаясь на несинхронизированные данные.

  • src/osd/osd_peering_pg.cpp:415-449 — эпоха PG может регрессировать [требует подтверждения мейнтейнера]. calc_object_states сбрасывает epoch = 0 и пересчитывает только из версий объектов в текущих листингах; персистентная reported_epoch из etcd не используется как нижняя граница. Если объекты с наивысшей эпохой пропали из листингов, эпоха падает ниже ранее использованной → теоретически возможна реанимация устаревшей реплики (с более высокой эпохой) поверх подтверждённой записи. Функция помечена самим автором FIXME: Write at least some tests.

  • src/blockstore/v1/read.cpp:810-817 — абсолютный индекс csum-блока в относительный массив (legacy store, csum_block_size > bitmap_granularity). При верификации чтения ещё не сброшенной big-write используется абсолютный индекс блока для доступа к per-write относительному массиву контрольных сумм → ложная «порча» или пропуск реальной порчи.

  • src/blockstore/v1/init.cpp:953-956 и src/blockstore/blockstore_heap.cpp:584-588 — потеря новых записей при torn-write в журнале. (init) Рваная small-write в начале блока журнала оставляет занулённый блок в середине журнала; следующий replay считает это концом журнала и тихо отбрасывает более новые зафиксированные записи. (heap) Стартовая перепроверка torn-write смотрит только на голову цепочки — несинхронизированные small-write под головой ROLLBACK/COMMIT не проверяются.

  • src/blockstore/multilist.cpp:185-197 — assert проверяет полный размер области вместо остатка в multilist_alloc_t::use(), позволяя диапазону зайти в следующую (занятую) область и испортить граничные теги аллокатора.

  • src/osd/osd_primary_sync.cpp:260 — очистка dirty_pgs клиента на завершении sync стирает маркеры записей, подтверждённых уже после снапшота sync, ослабляя механизм drop/replay при репире (потенциальная потеря подтверждённой записи) [требует подтверждения мейнтейнера].

Офлайн-инструменты диска (vitastor-disk) — порча при восстановлении/миграции

  • src/disk_tool/disk_tool_meta.cpp:578-580 — roundtrip dump-meta | write-meta зануляет ВСЕ контрольные суммы данных [проверено вручную]. dump-meta пишет ключ "block_csums" (:500), а write-meta читает e["data_csum"] (:578) — такого ключа нет ни в одном выводе. Per-entry CRC пересчитывается по занулённым байтам, поэтому метаданные «валидны», но затем каждое чтение данных на этом OSD валится по проверке checksum. На OSD с единственной копией (типичная причина ручной хирургии) — данные становятся нечитаемыми.

  • src/disk_tool/disk_tool_journal.cpp:531-582 — write-journal создаёт невалидный журнал на csum-сторе [частично проверено вручную]. Неверная формула числа csum-блоков (offset+len)/bs - len/bs (верная — в этом же файле на :236-238) + переставлены местами bitmap и контрольные суммы относительно on-disk формата. На старте OSD усекает журнал на первой small-write (теряя все последующие зафиксированные записи), а big-write протаскивают занулённые csum в метаданные.

  • src/disk_tool/disk_tool_resize.cpp:215-217 — free_last округляет вниз: при сжатии до невыровненного new_data_len последний data-блок ссылается на block_num == block_count (вне диапазона).

  • src/disk_tool/disk_tool_resize_auto.cpp:216-220 — resize --journal-size на журнале в разделе молча заменяет запрошенный размер на весь раздел; при общем разделе journal+meta (--force) запись журнала затирает область метаданных.

Память (удалённо/локально)

  • src/nfs/nfs_portmap.cpp:134 — удалённое переполнение кучи в NFS-портмаппере [проверено вручную]. malloc_or_die(sizeof(pmap3_mapping_list*) * N) выделяет 8 байт на элемент вместо sizeof(pmap3_mapping_list) (~56-64), а цикл пишет полные структуры. Достижимо неаутентифицированно (rpcinfo -p, prog=100000/vers=3/proc=4). v2-обработчик (:67) использует корректный sizeof — это опечатка sizeof(T*) вместо sizeof(T). Фикс: sizeof(pmap3_mapping_list).

  • src/nfs/proto/xdr_impl_inline.h:142-148 и :329-331 — обход проверок длины через 32-битное переполнение в xdr_bytes/xdr_array (длина с провода). entry_size*(*len) и 4 + ((len+3)&~3) переполняются в 32 бита → недо-аллокация с последующей записью сегментов → переполнение кучи в NFS/NFS-RDMA.

  • src/nfs/nfs_kv_write.cpp:421 — over-read кучи при redirect-записи за пределы размера shared-файла: дыра заполняется мусором из-за чтения за концом буфера.

  • src/client/qemu_driver.c:1041-1062 — use-after-free в block_status [требует подтверждения мейнтейнера]. client->last_bitmap заимствуется через coroutine-yield; конкурентный write/block_status освобождает его → UAF (multi-threaded AioContext QEMU).

Логика / CLI

  • src/cmd/cli_rm_data.cpp:64-69 — use-after-move: финальный SYNC после удаления никогда не отправляется [проверено вручную]. В агрегатной инициализации .objects = std::move(objects) (член 2) выполняется до .synced = !objects.size() || … (член 8), читающего уже опустошённый set → synced всегда true, get_immediate_commit не вызывается. На пулах immediate_commit != all удаления не стабилизируются; через vitastor-nfs (truncate/remove/rename отвечают OK) краш OSD в окне autosync откатывает удаления → объекты воскресают, при последующем расширении файла видны старые данные вместо нулей.

  • src/cmd/cli_merge.cpp:253-257 — continue_merge игнорирует ошибки валидации start_merge. Ошибки («Layer not found» и т.п.) ставят state=100, но вызывающий перетирает его на state=1, и при пустом sources машина состояний доходит до «Done … merged» с err=0. Последствие: во время долгого vitastor-cli rm конкурентное изменение цепочки (например, переименование базового слоя) делает merge ребёнка тихим no-op «успехом», после чего rm ребейзит ребёнка и удаляет данные слоёв → безвозвратная потеря блоков, существовавших только в этих слоях.

  • src/cmd/cli_rm.cpp:159-165 и :189-191 — порядок удаления в rm. (159) Inverse-child workflow уничтожает данные ребёнка до etcd-rename — прерывание/сбой транзакции оставляет ребёнка тихо читающим устаревшие данные родителя. (189) На non-immediate_commit пулах удаление source-слоя не синкается перед удалением, и потеря питания может зафиксировать удаления при откате слитой копии.

  • src/blockstore/v1/impl.cpp:439-474 — чанкованный online-решардинг оставляет clean_db_shards пустым для пула между паузами event-loop, пока флашер продолжает его мутировать → сброшенные обновления теряются/затеняются [требует подтверждения мейнтейнера].

  • src/client/cluster_client_wb.cpp:127-134 — потерянный dirty-run при рантайм-включении writeback: расщепление CACHE_DIRTY-пробега не-DIRTY записью инкрементит writeback_queue_size без добавления якоря → осиротевший dirty-run никогда не флашится.

  • src/client/nbd_proxy.cpp:1117-1121 — NBD_CMD_DISC делает exit(0) без cli->flush() (в отличие от ветки EPOLLRDHUP), теряя writeback-кэш при штатном отключении клиента.


? Средние (краш/DoS/восстановимые рассинхроны) — кратко

Достойны починки, но не дают тихой потери зафиксированных данных. Большинство — краш/abort OSD или утечки.

Файл:строка Суть
disk_tool_journal.cpp:510 bounds-check читает len до инициализации записи → переполнение new_journal_buf на oversized-дампе
blockstore_sync.cpp:108 assert(!lsn) рушит OSD при ретрае do_sync после WAIT_SQE (assert включён в release)
osd_primary_chain.cpp:64 ретрай chained-read по EIO/EDOM падает на assert(op_data->st < base_state)
blockstore_stable.cpp:55,70 busy-loop / пропуск записи мета-блока при ENOSPC/CHECK_SQES в середине батча stabilize
blockstore_write.cpp:330 big-write, завершившаяся во время in-flight fsync, идёт к записи метаданных без покрытия durability
blockstore_heap.cpp:2007 underflow to_compact_count после рестарта (декремент при GC без инкремента при загрузке)
blockstore_heap.cpp:666,1047,1766,2570 iterator-invalidation/UAF и NULL-разыменования в recovery/compaction/reshard heap
v1/read.cpp:367,493,688,100 OOB-чтение в последнем IOV_MAX-батче; утечка dyn_data refcount; неосвобождение data-блока 0; чтение журнала без пиннинга против trim
osd_peering_pg.cpp:162, osd_primary.cpp:508 32-битная маска 1 << role ломает EC с pg_size > 31 (UB-сдвиг) → healthy-объекты помечаются incomplete
osd_secondary.cpp:131, etcd_state_client.cpp:1208, osd.cpp:340, http_client.cpp:423 uncaught-исключения / 0ms-таймер / busy-spin на EAGAIN → abort или зависание event-loop (crash-loop на весь кластер)
osd_flush.cpp:95 ошибка remote-flush навсегда подвешивает PG в PG_REPEERING
ublk_server.cpp:739, nfs_proxy_rdma.cpp:589 разыменование get_sqe() без NULL-проверки (SIGSEGV при queue_depth≥1024); бесконечный цикл post_send
cli_dd.cpp:536,921 глотает ошибки fsync/SYNC (рапортует успех при сбое коммита); pipe-вход без count копирует один блок
cli_merge.cpp:641 UAF: SYNC-subop в delete_source не учтён в in_flight, snap_merger_t освобождается до колбэка
cluster_client.cpp:349 UAF: continue_ops() разыменовывает освобождённый op при синхронном завершении следующего SYNC
kv_db.cpp:236 32-битное переполнение в bounds-check read_string → ~4ГБ OOB-чтение на битом KV-блоке
blockstore_disk.cpp:246, blockstore_init.cpp:101, v1/init.cpp:348, disk_tool_resize_auto.cpp:119 невалидированный meta_area_size/частичный блок → abort на старте; суперблок пишется до инициализации меты; underflow block_count-done_cnt; устаревший data_size после resize
nbd_proxy.cpp:623 --readonly парсится, но не применяется — NBD всегда RW
node-binding/client.cc:61,1119-1120 UAF/NULL в Node.js-аддоне: uv_poll_t без uv_close; listing без Ref во время in-flight list_next
rdma_alloc.cpp:141 re-insert фрагмента в freelist с адресом базы региона вместо адреса фрагмента

Низкоприоритетные находки (43 шт., в основном недостижимые на практике переполнения, проблемы сообщения об ошибках, мелкие утечки) опущены ради читаемости — могу выгрузить отдельным списком при необходимости.


Примечания

  • Что особенно стоит починить первым делом: №1 (сжатие образа), №3 (ublk fsync) и №2 (удалённое переполнение OSD) — все три достижимы в штатной эксплуатации публичного облака и ведут к тихой потере/порче данных или компрометации.
  • Семейство «мёртвых ключей» (- vs _ во внутренних JSON, №1 и cli_rm.cpp:681): стоит провести аудит всех внутренних вызовов start_*, передающих JSON, мимо parse_config.
  • Семейство dump|write-инструментов: roundtrip vitastor-disk для метаданных и журнала на csum-сторах сейчас не сохраняет данные верно — это пути аварийного восстановления, где порча наиболее болезненна.
  • Находки с пометкой [требует подтверждения мейнтейнера] опираются на подтверждённую механику, но с узким окном срабатывания — их стоит проверить отдельно (желательно тестом), а не принимать на веру.
# Аудит Vitastor: баги, способные привести к повреждению или потере данных **Версия:** commit `c16f955a` (3.0.13+2), чистое рабочее дерево. **Область:** весь C/C++ код (`src/`, `node-binding/`), кроме вендоренного кода (`json11`, `cpp-btree`, `src/liburing`, `src/util/robin_hood.h`) и rust-крейта. JS-монитор (`mon/`) не рассматривался. **Методология:** код прочитан целиком пофайлово; каждый кандидат проверялся несколькими независимыми «адверсариальными» проходами, плюс ручная перепроверка по исходникам всех критических и части высоких находок. Отсев ложных срабатываний — приоритет. Помечено `[проверено вручную]` — то, что я лично подтвердил по коду. Помечено `[требует подтверждения мейнтейнера]` — реальный по механике дефект, но триггер узкий/сложный и я не смог довести трассу до 100%. --- ## ? Критические (тихая потеря/порча зафиксированных данных) ### 1. `vitastor-cli modify --resize <меньший размер>` удаляет ВСЕ данные образа, а не «хвост» `[проверено вручную]` **Файл:** `src/cmd/cli_modify.cpp:110-116` → `src/cmd/cli_rm_data.cpp:331-334` При сжатии образа `image_changer_t::loop` вызывает `start_rm_data`, передавая ключи **через дефис**: ```cpp cb = parent->start_rm_data(json11::Json::object { ... { "min-offset", ((new_size+4095)/4096)*4096 }, // дефис! { "down-ok", down_ok }, }); ``` А `start_rm_data` читает ключи **через подчёркивание**: `cfg["min_offset"]` (строка 333), `cfg["down_ok"]` (331). Нормализация `-`→`_` (`cli_tool_t::parse_config`, `cli_common.cpp:104-116`) применяется **только** к argv верхнего уровня в `cli.cpp:331` и не применяется к этому внутреннему JSON. В результате `min_offset` отсутствует → json11 возвращает null → `uint64_value() == 0`. При `min_offset==0 && max_offset==0` код `rm_inode_t` считает это «без границы» и удаляет **все** объекты инода (`cli_rm_data.cpp:70-73` и условие на `:97` `stripe >= 0 && (!0 || …)` всегда истинно). После этого `cfg.size` уменьшается, транзакция в etcd коммитится, печатается «Image … modified». **Триггер:** документированная операция — `vitastor-cli modify myimage --resize 500G` на образе 1 ТБ после уменьшения ФС внутри. Также достижимо через `vitastor-nfs` block-mode SETATTR-truncate (`nfs_block.cpp:192-199`) и `vitastor-cli dd conv=trunc` на существующий бо́льший образ. **Последствие:** полная тихая потеря всех данных образа при «безопасной» операции сжатия; команда рапортует успех. **Фикс:** передавать `min_offset`/`down_ok` (подчёркивание) — как это уже корректно делает `nfs_kv_setattr.cpp:133`. > Тот же «мёртвый ключ» есть в `cli_rm.cpp:680-681` (`down-ok`/`fsync-interval`), но там `min-offset` не передаётся (удаляется весь инод по замыслу), поэтому игнорируется лишь `--down-ok` — это «fail-safe», не потеря данных. ### 2. Удалённое неаутентифицированное переполнение кучи в OSD через `OSD_OP_SHOW_CONFIG` `[проверено вручную]` **Файл:** `src/client/msgr_receive.cpp:318-327`, `handle_op_hdr` ```cpp else if (cur_op->req.hdr.opcode == OSD_OP_SHOW_CONFIG) { if (cur_op->req.show_conf.json_len > 0) { cur_op->buf = malloc_or_die(cur_op->req.show_conf.json_len+1); // json_len = UINT64_MAX → malloc(0) ((uint8_t*)cur_op->buf)[cur_op->req.show_conf.json_len] = 0; // запись по buf[-1] cl->recv_list.push_back(cur_op->buf, cur_op->req.show_conf.json_len); // iovec длиной 2^64-1 } cl->read_remaining = cur_op->req.show_conf.json_len; // int ← uint64: -1 } ``` `json_len` — 64-битное поле с провода (`osd_ops.h:183`), не валидируется. При `json_len = 0xFFFFFFFFFFFFFFFF`: `json_len+1` переполняется в 0 → `malloc_or_die(0)` отдаёт крошечный буфер; в `recv_list` кладётся iovec длиной `2^64-1`. `read_remaining` — `int` (`messenger.h:79`), становится `-1`, поэтому op помечается «готовым» через ветку `else`, **не сбрасывая `recv_list`** (он был сброшен в `handle_finished_read:217` до вызова). На следующей итерации `handle_read_buffer` остаток принятых байт (до ~64 КБ за один recv) `memcpy`-ится в этот гигантский iovec поверх `malloc(0)`-буфера (`msgr_receive.cpp:185`). Проверка магии в `handle_finished_read:222` (`SECONDARY_OSD_OP_MAGIC`) **не требует аутентификации**; `accept_connections` принимает любого TCP-пира. В вашей модели угроз (tenant-VM имеют сетевой доступ к data-портам OSD через QEMU/NBD-драйвер) это критично. **Последствие:** удалённое переполнение кучи на единственном event-loop OSD → краш/DoS, порча соседних буферов (в т.ч. данных конкурентных записей до сброса на диск), потенциально RCE. **Фикс:** валидировать `json_len` (и вообще все длины с провода) разумным верхним пределом до аллокации. ### 3. ublk в режиме образа (`--image`, дефолт и дефолт CSI): fsync гостя не доходит до OSD `[проверено вручную]` **Файл:** `src/client/ublk_server.cpp:285` ```cpp const bool writeback = !cli->get_immediate_commit(inode); // inode == 0 в режиме образа! auto pool_it = cli->st_cli.pool_config.find(INODE_POOL(inode ? inode : watch->cfg.num)); // ← правильный fallback строкой ниже ``` В режиме образа `inode` всегда `0` (`ublk_server.cpp:196-201`), реальный инод — в `watch->cfg.num`. `get_immediate_commit(0)` из-за раннего `if (!pool_id) return true;` (`cluster_client.cpp:631-632`) возвращает `true` → `writeback=false` → в `add_device` не выставляется `UBLK_ATTR_VOLATILE_CACHE` (`ublk_server.cpp:318`). Ядро считает устройство write-through и **не шлёт FLUSH**, поэтому обработчик `UBLK_IO_OP_FLUSH → OSD_OP_SYNC` (`ublk_server.cpp:772-775`) никогда не вызывается. То, что соседняя строка 286 уже использует fallback `inode ? inode : watch->cfg.num`, доказывает: автор знал про `inode==0` и забыл применить fallback в строке 285 (предыдущий фикс `85c7e3bd` чинил полярность этой строки, но пропустил случай образа). **Триггер:** пул с `immediate_commit=none/small` (документированная норма для HDD и SSD без конденсаторов) + том примонтирован `vitastor-ublk map --image …` (или через Kubernetes CSI, где ublk теперь дефолт). Гость делает `fsync()` → возвращает успех, данные не синкнуты на OSD → потеря питания/краш OSD → подтверждённые fsync-ом данные теряются. **Последствие:** тихая потеря данных, подтверждённых `fsync()`, на любом пуле кроме `immediate_commit=all`; повреждение журналов гостевых ФС. **Фикс:** `cli->get_immediate_commit(inode ? inode : watch->cfg.num)`. ### 4. `vitastor-disk resize` тихо рушит метаданные heap-OSD при области метаданных > 4 ГиБ `[проверено вручную]` **Файл:** `src/disk_tool/disk_tool_resize.cpp:565,594,598,616` + `src/blockstore/blockstore_heap.h:364` `new_meta_pos` — `uint64_t` (строка 565), но `blockstore_heap_t::fill_block_empty_space(uint8_t*, uint32_t pos)` принимает **`uint32_t`**. На вызовах 594/598/616 позиция ≥ 4 ГиБ тихо усекается по модулю 2³². Внутри (`blockstore_heap.cpp:2367-2384`) функция пишет 4-байтовый маркер конца блока `{size, BS_HEAP_FREE_SPACE}` по `buffer + (pos % meta_block_size)`, продвинув `buffer` на усечённый индекс блока. То есть маркер «дальше блок свободен» для позиции ≥ 2³² попадает в **низкий** блок метаданных, где уже лежат реальные записи (а при ровно 2³² — в суперблок). Копии самих записей (`memcpy` на строке 624) используют корректную 64-битную арифметику, поэтому портятся именно маркеры — загрузчик молча считает заполненные низкие блоки пустыми. **Триггер:** heap-формат (Vitastor 3.0), `meta_area_size > 4 ГиБ` — штатно для OSD ≥ 4 ТБ со 128К блоками и crc32c (min_meta_len ~5.8 ГиБ, а `vitastor-disk prepare` резервирует 2× минимума). Админ запускает `vitastor-disk resize`/`raw-resize`. **Последствие:** массовая тихая потеря зафиксированных объектов на resized-OSD (часть блоков загрузчик видит пустыми, без ошибки; либо «Metadata is corrupt»). **Фикс:** объявить `pos` как `uint64_t`/`size_t` в `fill_block_empty_space`. ### 5. Повторная отправка устаревшего writeback-флаша перезаписывает уже подтверждённые fsync-ом данные `[требует подтверждения мейнтейнера]` **Файл:** `src/client/cluster_client_wb.cpp:300-364`, `src/client/cluster_client.cpp:196-211` Подтверждённая механика: в `calc_wait` два флаш-op (`OP_FLUSH_BUFFER`) не упорядочиваются друг относительно друга — условие на строке 204 требует `!(op->flags & OP_FLUSH_BUFFER)`. При сбое флаша `F1` (рестарт OSD, `-EPIPE`) он паркуется на `client_retry_interval` (`cluster_client.cpp:1600-1606`), при этом захваченный им буфер со старыми данными жив (refcnt в `flushed_buffers`, `op->iov` указывает на него). Свежий перекрывающий флаш `F2` имеет `retry_after=0` и отправляется первым; после его коммита запоздавший `F1` повторно пишет старые данные тем же диапазоном без версии/CAS. **Триггер (узкое окно):** `client_enable_writeback=true` (QEMU `cache=writeback`, NBD, ublk, NFS); флаш по лимиту буфера без хвостового SYNC; рестарт primary OSD; перезапись того же горячего блока + `fsync` в окне ретрая. **Последствие:** старая версия блока становится durable после успешного `fsync` новой версии (например, commit-блок журнала гостевой ФС) → тихая порча, видимая при последующих чтениях. **Фикс:** упорядочить повторную отправку флаша относительно более новых перекрывающихся флашей того же диапазона. --- ## ? Высокие ### Хранилище / OSD - **`src/blockstore/blockstore_disk.cpp:543-556` — `trim_data` дискардит ЖИВЫЕ блоки** `[проверено вручную]`. После выравнивания по `discard_granularity` пустой диапазон делает `continue`, **пропуская `j = i+1`** (строка 556). `j` остаётся на старом начале пробега, и следующий `BLKDISCARD` накрывает промежуточный **используемый** блок → физическое уничтожение зафиксированных данных (на чтении — нули; с checksum — EIO). Достижимо через `vitastor-disk trim` и `discard_on_start`. **Фикс:** продвигать `j` даже при пустом выровненном диапазоне. - **`src/osd/osd_rmw.cpp:1211-1218` — переполнение кучи в скрабе degraded EC-объекта**. `ec_check_combination` индексирует `tmp_buf` по роли `[0, pg_size)`, но `ec_find_good` (`:1341`) выделяет `tmp_buf` размером `stripe_count*(chunk_size+bitmap_size)`, где `stripe_count` = число присутствующих чанков `< pg_size`. Для EC с ≥2 parity и объектом, потерявшим 1..(parity-1) чанков, восстановление пишет `chunk_size` (≥128К) байт за пределы аллокации. **Последствие:** краш OSD при скрабе любого degraded EC-объекта; возможна тихая порча соседней кучи. - **`src/osd/osd_scrub.cpp:630-635` — скраб снимает `LOC_CORRUPTED` с всё ещё битых чанков**. Массив `stripes` индексируется по `chunk.role` вместо `i`, плюс нет проверки `!read_error` → для EC ещё и OOB-чтение. **Последствие:** скраб может пометить реально повреждённый чанк как хороший / неверно «починить». - **`src/osd/osd_primary_chain.cpp:131-148` — неверная реконструкция bitmap при цепочечном (snapshot) чтении EC**. В качестве источников декодирования используются никогда не запрошенные нулевые bitmap недоступных/незагруженных чанков → неверная карта присутствия данных → неверные результаты чтения снапшот-цепочек. - **`src/blockstore/blockstore_sync.cpp:30-88` — sync может быть подтверждён без нужного fsync** `[требует подтверждения мейнтейнера]`. Дедупликация fsync по совпадению fd: при раскладке `data_fd == journal_fd != meta_fd` и наличии незасинканных data-записей без buffer-записей ни data-, ни journal-fsync не выполняется (data-fsync пропущен по `data_fd==journal_fd`, journal-fsync — по `unsynced_buffer_write_count==0`). **Последствие:** в нестандартной раскладке дисков метаданные становятся durable, ссылаясь на несинхронизированные данные. - **`src/osd/osd_peering_pg.cpp:415-449` — эпоха PG может регрессировать** `[требует подтверждения мейнтейнера]`. `calc_object_states` сбрасывает `epoch = 0` и пересчитывает только из версий объектов в текущих листингах; персистентная `reported_epoch` из etcd не используется как нижняя граница. Если объекты с наивысшей эпохой пропали из листингов, эпоха падает ниже ранее использованной → теоретически возможна реанимация устаревшей реплики (с более высокой эпохой) поверх подтверждённой записи. Функция помечена самим автором `FIXME: Write at least some tests`. - **`src/blockstore/v1/read.cpp:810-817` — абсолютный индекс csum-блока в относительный массив** (legacy store, `csum_block_size > bitmap_granularity`). При верификации чтения ещё не сброшенной big-write используется абсолютный индекс блока для доступа к per-write **относительному** массиву контрольных сумм → ложная «порча» или пропуск реальной порчи. - **`src/blockstore/v1/init.cpp:953-956` и `src/blockstore/blockstore_heap.cpp:584-588` — потеря новых записей при torn-write в журнале**. (init) Рваная small-write в начале блока журнала оставляет занулённый блок в середине журнала; следующий replay считает это концом журнала и **тихо отбрасывает более новые зафиксированные записи**. (heap) Стартовая перепроверка torn-write смотрит только на голову цепочки — несинхронизированные small-write под головой ROLLBACK/COMMIT не проверяются. - **`src/blockstore/multilist.cpp:185-197` — assert проверяет полный размер области вместо остатка** в `multilist_alloc_t::use()`, позволяя диапазону зайти в следующую (занятую) область и испортить граничные теги аллокатора. - **`src/osd/osd_primary_sync.cpp:260` — очистка `dirty_pgs` клиента на завершении sync** стирает маркеры записей, подтверждённых уже после снапшота sync, ослабляя механизм drop/replay при репире (потенциальная потеря подтверждённой записи) `[требует подтверждения мейнтейнера]`. ### Офлайн-инструменты диска (`vitastor-disk`) — порча при восстановлении/миграции - **`src/disk_tool/disk_tool_meta.cpp:578-580` — roundtrip `dump-meta | write-meta` зануляет ВСЕ контрольные суммы данных** `[проверено вручную]`. `dump-meta` пишет ключ `"block_csums"` (`:500`), а `write-meta` читает `e["data_csum"]` (`:578`) — такого ключа нет ни в одном выводе. Per-entry CRC пересчитывается по занулённым байтам, поэтому метаданные «валидны», но затем каждое чтение данных на этом OSD валится по проверке checksum. На OSD с единственной копией (типичная причина ручной хирургии) — данные становятся нечитаемыми. - **`src/disk_tool/disk_tool_journal.cpp:531-582` — `write-journal` создаёт невалидный журнал на csum-сторе** `[частично проверено вручную]`. Неверная формула числа csum-блоков `(offset+len)/bs - len/bs` (верная — в этом же файле на `:236-238`) + переставлены местами bitmap и контрольные суммы относительно on-disk формата. На старте OSD усекает журнал на первой small-write (теряя все последующие зафиксированные записи), а big-write протаскивают занулённые csum в метаданные. - **`src/disk_tool/disk_tool_resize.cpp:215-217` — `free_last` округляет вниз**: при сжатии до невыровненного `new_data_len` последний data-блок ссылается на `block_num == block_count` (вне диапазона). - **`src/disk_tool/disk_tool_resize_auto.cpp:216-220` — `resize --journal-size` на журнале в разделе** молча заменяет запрошенный размер на весь раздел; при общем разделе journal+meta (`--force`) запись журнала затирает область метаданных. ### Память (удалённо/локально) - **`src/nfs/nfs_portmap.cpp:134` — удалённое переполнение кучи в NFS-портмаппере** `[проверено вручную]`. `malloc_or_die(sizeof(pmap3_mapping_list*) * N)` выделяет 8 байт на элемент вместо `sizeof(pmap3_mapping_list)` (~56-64), а цикл пишет полные структуры. Достижимо неаутентифицированно (`rpcinfo -p`, prog=100000/vers=3/proc=4). v2-обработчик (`:67`) использует корректный sizeof — это опечатка `sizeof(T*)` вместо `sizeof(T)`. **Фикс:** `sizeof(pmap3_mapping_list)`. - **`src/nfs/proto/xdr_impl_inline.h:142-148 и :329-331` — обход проверок длины через 32-битное переполнение** в `xdr_bytes`/`xdr_array` (длина с провода). `entry_size*(*len)` и `4 + ((len+3)&~3)` переполняются в 32 бита → недо-аллокация с последующей записью сегментов → переполнение кучи в NFS/NFS-RDMA. - **`src/nfs/nfs_kv_write.cpp:421` — over-read кучи** при redirect-записи за пределы размера shared-файла: дыра заполняется мусором из-за чтения за концом буфера. - **`src/client/qemu_driver.c:1041-1062` — use-after-free в `block_status`** `[требует подтверждения мейнтейнера]`. `client->last_bitmap` заимствуется через coroutine-yield; конкурентный write/block_status освобождает его → UAF (multi-threaded AioContext QEMU). ### Логика / CLI - **`src/cmd/cli_rm_data.cpp:64-69` — use-after-move: финальный SYNC после удаления никогда не отправляется** `[проверено вручную]`. В агрегатной инициализации `.objects = std::move(objects)` (член 2) выполняется до `.synced = !objects.size() || …` (член 8), читающего уже опустошённый set → `synced` всегда `true`, `get_immediate_commit` не вызывается. На пулах `immediate_commit != all` удаления не стабилизируются; через `vitastor-nfs` (truncate/remove/rename отвечают OK) краш OSD в окне autosync откатывает удаления → **объекты воскресают**, при последующем расширении файла видны старые данные вместо нулей. - **`src/cmd/cli_merge.cpp:253-257` — `continue_merge` игнорирует ошибки валидации `start_merge`**. Ошибки («Layer not found» и т.п.) ставят `state=100`, но вызывающий перетирает его на `state=1`, и при пустом `sources` машина состояний доходит до «Done … merged» с `err=0`. **Последствие:** во время долгого `vitastor-cli rm` конкурентное изменение цепочки (например, переименование базового слоя) делает merge ребёнка тихим no-op «успехом», после чего `rm` ребейзит ребёнка и удаляет данные слоёв → безвозвратная потеря блоков, существовавших только в этих слоях. - **`src/cmd/cli_rm.cpp:159-165 и :189-191` — порядок удаления в `rm`**. (159) Inverse-child workflow уничтожает данные ребёнка **до** etcd-rename — прерывание/сбой транзакции оставляет ребёнка тихо читающим устаревшие данные родителя. (189) На non-`immediate_commit` пулах удаление source-слоя не синкается перед удалением, и потеря питания может зафиксировать удаления при откате слитой копии. - **`src/blockstore/v1/impl.cpp:439-474` — чанкованный online-решардинг** оставляет `clean_db_shards` пустым для пула между паузами event-loop, пока флашер продолжает его мутировать → сброшенные обновления теряются/затеняются `[требует подтверждения мейнтейнера]`. - **`src/client/cluster_client_wb.cpp:127-134` — потерянный dirty-run** при рантайм-включении writeback: расщепление CACHE_DIRTY-пробега не-DIRTY записью инкрементит `writeback_queue_size` без добавления якоря → осиротевший dirty-run никогда не флашится. - **`src/client/nbd_proxy.cpp:1117-1121` — `NBD_CMD_DISC` делает `exit(0)` без `cli->flush()`** (в отличие от ветки EPOLLRDHUP), теряя writeback-кэш при штатном отключении клиента. --- ## ? Средние (краш/DoS/восстановимые рассинхроны) — кратко Достойны починки, но не дают тихой потери зафиксированных данных. Большинство — краш/abort OSD или утечки. | Файл:строка | Суть | |---|---| | `disk_tool_journal.cpp:510` | bounds-check читает `len` до инициализации записи → переполнение `new_journal_buf` на oversized-дампе | | `blockstore_sync.cpp:108` | `assert(!lsn)` рушит OSD при ретрае do_sync после WAIT_SQE (assert включён в release) | | `osd_primary_chain.cpp:64` | ретрай chained-read по EIO/EDOM падает на `assert(op_data->st < base_state)` | | `blockstore_stable.cpp:55,70` | busy-loop / пропуск записи мета-блока при ENOSPC/CHECK_SQES в середине батча stabilize | | `blockstore_write.cpp:330` | big-write, завершившаяся во время in-flight fsync, идёт к записи метаданных без покрытия durability | | `blockstore_heap.cpp:2007` | underflow `to_compact_count` после рестарта (декремент при GC без инкремента при загрузке) | | `blockstore_heap.cpp:666,1047,1766,2570` | iterator-invalidation/UAF и NULL-разыменования в recovery/compaction/reshard heap | | `v1/read.cpp:367,493,688,100` | OOB-чтение в последнем IOV_MAX-батче; утечка dyn_data refcount; неосвобождение data-блока 0; чтение журнала без пиннинга против trim | | `osd_peering_pg.cpp:162`, `osd_primary.cpp:508` | 32-битная маска `1 << role` ломает EC с `pg_size > 31` (UB-сдвиг) → healthy-объекты помечаются incomplete | | `osd_secondary.cpp:131`, `etcd_state_client.cpp:1208`, `osd.cpp:340`, `http_client.cpp:423` | uncaught-исключения / 0ms-таймер / busy-spin на EAGAIN → abort или зависание event-loop (crash-loop на весь кластер) | | `osd_flush.cpp:95` | ошибка remote-flush навсегда подвешивает PG в `PG_REPEERING` | | `ublk_server.cpp:739`, `nfs_proxy_rdma.cpp:589` | разыменование `get_sqe()` без NULL-проверки (SIGSEGV при queue_depth≥1024); бесконечный цикл post_send | | `cli_dd.cpp:536,921` | глотает ошибки fsync/SYNC (рапортует успех при сбое коммита); pipe-вход без count копирует один блок | | `cli_merge.cpp:641` | UAF: SYNC-subop в `delete_source` не учтён в `in_flight`, `snap_merger_t` освобождается до колбэка | | `cluster_client.cpp:349` | UAF: `continue_ops()` разыменовывает освобождённый op при синхронном завершении следующего SYNC | | `kv_db.cpp:236` | 32-битное переполнение в bounds-check `read_string` → ~4ГБ OOB-чтение на битом KV-блоке | | `blockstore_disk.cpp:246`, `blockstore_init.cpp:101`, `v1/init.cpp:348`, `disk_tool_resize_auto.cpp:119` | невалидированный `meta_area_size`/частичный блок → abort на старте; суперблок пишется до инициализации меты; underflow `block_count-done_cnt`; устаревший `data_size` после resize | | `nbd_proxy.cpp:623` | `--readonly` парсится, но не применяется — NBD всегда RW | | `node-binding/client.cc:61,1119-1120` | UAF/NULL в Node.js-аддоне: `uv_poll_t` без `uv_close`; listing без Ref во время in-flight `list_next` | | `rdma_alloc.cpp:141` | re-insert фрагмента в freelist с адресом базы региона вместо адреса фрагмента | *Низкоприоритетные находки (43 шт., в основном недостижимые на практике переполнения, проблемы сообщения об ошибках, мелкие утечки) опущены ради читаемости — могу выгрузить отдельным списком при необходимости.* --- ## Примечания - **Что особенно стоит починить первым делом:** №1 (сжатие образа), №3 (ublk fsync) и №2 (удалённое переполнение OSD) — все три достижимы в штатной эксплуатации публичного облака и ведут к тихой потере/порче данных или компрометации. - **Семейство «мёртвых ключей»** (`-` vs `_` во внутренних JSON, №1 и `cli_rm.cpp:681`): стоит провести аудит всех внутренних вызовов `start_*`, передающих JSON, мимо `parse_config`. - **Семейство `dump|write`-инструментов**: roundtrip `vitastor-disk` для метаданных и журнала на csum-сторах сейчас не сохраняет данные верно — это пути аварийного восстановления, где порча наиболее болезненна. - Находки с пометкой `[требует подтверждения мейнтейнера]` опираются на подтверждённую механику, но с узким окном срабатывания — их стоит проверить отдельно (желательно тестом), а не принимать на веру.
Owner
  1. vitastor-cli modify --resize <меньший размер> удаляет ВСЕ данные образа, а не «хвост» [проверено вручную]

Смешной однако баг. Исправил, добавил тест

  1. Удалённое неаутентифицированное переполнение кучи в OSD через OSD_OP_SHOW_CONFIG [проверено вручную]

Ладно, уже десятый раз про это все нейронки пишут, перенёс фиксы этого в 3.0 из ветки openssl

  1. ublk в режиме образа (--image, дефолт и дефолт CSI): fsync гостя не доходит до OSD [проверено вручную]

Поправил

  1. vitastor-disk resize тихо рушит метаданные heap-OSD при области метаданных > 4 ГиБ [проверено вручную]

Поправил

  1. Повторная отправка устаревшего writeback-флаша перезаписывает уже подтверждённые fsync-ом данные [требует подтверждения мейнтейнера]

TODO Надо допроверить

src/blockstore/blockstore_disk.cpp:543-556 — trim_data дискардит ЖИВЫЕ блоки

Реально, поправил, но это было только если передать параметр --discard_granularity > 4096.

src/osd/osd_rmw.cpp:1211-1218 — переполнение кучи в скрабе degraded EC-объекта.

Тоже уже десятый раз нейронки ругаются на эти скрабы - поправил в репорте от GPT 5.5 от Лисы, тут то же самое, регрессионный тест добавил.

src/osd/osd_scrub.cpp:630-635 — скраб снимает LOC_CORRUPTED с всё ещё битых чанков.

Аналогично, GPT-5-5 от Лисы тоже это нашёл, поправлено и добавлен регресс-тест

src/osd/osd_primary_chain.cpp:131-148 — неверная реконструкция bitmap при цепочечном (snapshot) чтении EC

Годно, поправил, добавил регресс-тест

src/blockstore/blockstore_sync.cpp:30-88 — sync может быть подтверждён без нужного fsync

GPT-5-5 на это тоже ругнулся, я поправил, но фактически это не могло триггерить баг т.к. это могло сработать только при data_fd == journal_fd != meta_fd. А так в жизни не бывает

src/osd/osd_peering_pg.cpp:415-449 — эпоха PG может регрессировать

TODO Тут надо допроверить, возможно и есть смысл поправить

src/blockstore/v1/read.cpp:810-817 — абсолютный индекс csum-блока в относительный массив

Мало деталей, но уже в процессе дебага нашёл сам, добавил тестов (первые тесты на старый стор), поправил

src/blockstore/v1/init.cpp:953-956 и src/blockstore/blockstore_heap.cpp:584-588 - потеря новых записей при torn-write в журнале

Журнал специально так сделан, тут это не баг.

src/blockstore/multilist.cpp:185-197 — assert проверяет полный размер области вместо остатка в multilist_alloc_t::use(), позволяя диапазону зайти в следующую (занятую) область и испортить граничные теги аллокатора.

Формально это конечно был тоже баг, в use(), но фактически он не мог триггериться в жизни, т.к. в жизни use() идёт по оффсету полученному через find_free. А там бага нет. Но поправил и тоже нагенерил тест.

src/osd/osd_primary_sync.cpp:260 — очистка dirty_pgs клиента на завершении sync стирает маркеры записей, подтверждённых уже после снапшота sync

TODO Надо допроверить, пока не могу ничего сказать.

Дальше тоже пока не посмотрел, смотрим дальше)

>1. vitastor-cli modify --resize <меньший размер> удаляет ВСЕ данные образа, а не «хвост» [проверено вручную] Смешной однако баг. Исправил, добавил тест >2. Удалённое неаутентифицированное переполнение кучи в OSD через OSD_OP_SHOW_CONFIG [проверено вручную] Ладно, уже десятый раз про это все нейронки пишут, перенёс фиксы этого в 3.0 из ветки openssl >3. ublk в режиме образа (--image, дефолт и дефолт CSI): fsync гостя не доходит до OSD [проверено вручную] Поправил >4. vitastor-disk resize тихо рушит метаданные heap-OSD при области метаданных > 4 ГиБ [проверено вручную] Поправил >5. Повторная отправка устаревшего writeback-флаша перезаписывает уже подтверждённые fsync-ом данные [требует подтверждения мейнтейнера] **TODO** Надо допроверить >src/blockstore/blockstore_disk.cpp:543-556 — trim_data дискардит ЖИВЫЕ блоки Реально, поправил, но это было только если передать параметр --discard_granularity > 4096. >src/osd/osd_rmw.cpp:1211-1218 — переполнение кучи в скрабе degraded EC-объекта. Тоже уже десятый раз нейронки ругаются на эти скрабы - поправил в репорте от GPT 5.5 от Лисы, тут то же самое, регрессионный тест добавил. >src/osd/osd_scrub.cpp:630-635 — скраб снимает LOC_CORRUPTED с всё ещё битых чанков. Аналогично, GPT-5-5 от Лисы тоже это нашёл, поправлено и добавлен регресс-тест >src/osd/osd_primary_chain.cpp:131-148 — неверная реконструкция bitmap при цепочечном (snapshot) чтении EC Годно, поправил, добавил регресс-тест >src/blockstore/blockstore_sync.cpp:30-88 — sync может быть подтверждён без нужного fsync GPT-5-5 на это тоже ругнулся, я поправил, но фактически это не могло триггерить баг т.к. это могло сработать только при data_fd == journal_fd != meta_fd. А так в жизни не бывает >src/osd/osd_peering_pg.cpp:415-449 — эпоха PG может регрессировать **TODO** Тут надо допроверить, возможно и есть смысл поправить >src/blockstore/v1/read.cpp:810-817 — абсолютный индекс csum-блока в относительный массив Мало деталей, но уже в процессе дебага нашёл сам, добавил тестов (первые тесты на старый стор), поправил >src/blockstore/v1/init.cpp:953-956 и src/blockstore/blockstore_heap.cpp:584-588 - потеря новых записей при torn-write в журнале Журнал специально так сделан, тут это не баг. >src/blockstore/multilist.cpp:185-197 — assert проверяет полный размер области вместо остатка в multilist_alloc_t::use(), позволяя диапазону зайти в следующую (занятую) область и испортить граничные теги аллокатора. Формально это конечно был тоже баг, в use(), но фактически он не мог триггериться в жизни, т.к. в жизни use() идёт по оффсету полученному через find_free. А там бага нет. Но поправил и тоже нагенерил тест. >src/osd/osd_primary_sync.cpp:260 — очистка dirty_pgs клиента на завершении sync стирает маркеры записей, подтверждённых уже после снапшота sync **TODO** Надо допроверить, пока не могу ничего сказать. Дальше тоже пока не посмотрел, смотрим дальше)
Owner

Дальше:

src/disk_tool/disk_tool_meta.cpp:578-580 — roundtrip dump-meta | write-meta зануляет ВСЕ контрольные суммы данных
src/disk_tool/disk_tool_journal.cpp:531-582 — write-journal создаёт невалидный журнал на csum-сторе

Эти два замечания - это было даже ещё не всё. Добавил тест dump|load для обоих сторов и поправил в итоге 4 бага.

src/disk_tool/disk_tool_resize.cpp:215-217 — free_last округляет вниз

Поправил, тоже добавил тест. Ну кейс узкий, но действительно разница не-кратных размеру блока размеров области данных в части последнего блока обрабатывалась некорректно.

src/disk_tool/disk_tool_resize_auto.cpp:216-220 — resize --journal-size на журнале в разделе молча заменяет запрошенный размер на весь раздел; при общем разделе journal+meta (--force) запись журнала затирает область метаданных.

Тут всё же не баг, auto-resize это упрощённая команда с ограниченными возможностями, там можно только либо переместить журнал/мету на отдельные девайсы или вернуть обратно на общий с данными, либо уменьшить/увеличить журнал, либо данные, на общем устройстве данных. Переместить на один и тот же девайс журнал и мету, ещё и задав вручную их размер, там нельзя и не должно быть "льзя". Также заставить его сделать пересекающиеся журнал и мету невозможно даже с --force.

Дальше: >src/disk_tool/disk_tool_meta.cpp:578-580 — roundtrip dump-meta | write-meta зануляет ВСЕ контрольные суммы данных >src/disk_tool/disk_tool_journal.cpp:531-582 — write-journal создаёт невалидный журнал на csum-сторе Эти два замечания - это было даже ещё не всё. Добавил тест dump|load для обоих сторов и поправил в итоге 4 бага. >src/disk_tool/disk_tool_resize.cpp:215-217 — free_last округляет вниз Поправил, тоже добавил тест. Ну кейс узкий, но действительно разница не-кратных размеру блока размеров области данных в части последнего блока обрабатывалась некорректно. >src/disk_tool/disk_tool_resize_auto.cpp:216-220 — resize --journal-size на журнале в разделе молча заменяет запрошенный размер на весь раздел; при общем разделе journal+meta (--force) запись журнала затирает область метаданных. Тут всё же не баг, auto-resize это упрощённая команда с ограниченными возможностями, там можно только либо переместить журнал/мету на отдельные девайсы или вернуть обратно на общий с данными, либо уменьшить/увеличить журнал, либо данные, на общем устройстве данных. Переместить на один и тот же девайс журнал и мету, ещё и задав вручную их размер, там нельзя и не должно быть "льзя". Также заставить его сделать пересекающиеся журнал и мету невозможно даже с --force.
Owner

Память (удалённо/локально)

src/nfs/nfs_portmap.cpp:134 — удалённое переполнение кучи в NFS-портмаппере

Поправил

src/nfs/proto/xdr_impl_inline.h:142-148 и :329-331 — обход проверок длины через 32-битное переполнение

Поправил, ну это конечно кейс взлома нфс сервера :)

src/nfs/nfs_kv_write.cpp:421 — over-read кучи при redirect-записи за пределы размера shared-файла: дыра заполняется мусором из-за чтения за концом буфера.

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

src/client/qemu_driver.c:1041-1062 — use-after-free в block_status [требует подтверждения мейнтейнера]. client->last_bitmap заимствуется через coroutine-yield; конкурентный write/block_status освобождает его → UAF (multi-threaded AioContext QEMU).

Тут кажется бага нет, т.к. драйвер не многопоточный.

Память (удалённо/локально) >src/nfs/nfs_portmap.cpp:134 — удалённое переполнение кучи в NFS-портмаппере Поправил >src/nfs/proto/xdr_impl_inline.h:142-148 и :329-331 — обход проверок длины через 32-битное переполнение Поправил, ну это конечно кейс взлома нфс сервера :) >src/nfs/nfs_kv_write.cpp:421 — over-read кучи при redirect-записи за пределы размера shared-файла: дыра заполняется мусором из-за чтения за концом буфера. Попытался добавить тест, но сходу не смог триггернуть из внешнего мира чтобы буфер забился мусором. Но баг был, поправил >src/client/qemu_driver.c:1041-1062 — use-after-free в block_status [требует подтверждения мейнтейнера]. client->last_bitmap заимствуется через coroutine-yield; конкурентный write/block_status освобождает его → UAF (multi-threaded AioContext QEMU). Тут кажется бага нет, т.к. драйвер не многопоточный.
Owner

Логика / CLI

src/cmd/cli_rm_data.cpp:64-69 — use-after-move: финальный SYNC после удаления никогда не отправляется

Поправил

src/cmd/cli_merge.cpp:253-257 — continue_merge игнорирует ошибки валидации start_merge

Тоже смешной баг, поправил

src/cmd/cli_rm.cpp:159-165 и :189-191 — порядок удаления в rm. (159) Inverse-child workflow уничтожает данные ребёнка до etcd-rename

Тут как бы корректно, конкурентных операций одновременно с rm inverse-child не должно быть. Ну то есть логично, если ты решил что-то нехорошее сделать с парентом, пока его переименовываешь в чайлда, ничего хорошего не будет.

При простых прерываниях тоже ничего плохого не будет - т.к. сначала мержится дочерний в родительский, потом удаляются данные дочернего, потом переименовывается родительский в дочерний. Писателей в этот момент быть не должно (механизма жёстко запретить это нет, но идея такая).

src/blockstore/v1/impl.cpp:439-474 — чанкованный online-решардинг оставляет clean_db_shards пустым для пула между паузами event-loop, пока флашер продолжает его мутировать → сброшенные обновления теряются/затеняются [требует подтверждения мейнтейнера].

ГМ, однако палево А как бы это и для нового стора тоже не было актуально... Надо поправить для обоих. TODO

UPD Отключил этот chunked resharding вообще нафиг пока что. Нужно сначала поправить, тестов написать, потом включать обратно.

src/client/cluster_client_wb.cpp:127-134 — потерянный dirty-run при рантайм-включении writeback: расщепление CACHE_DIRTY-пробега не-DIRTY записью инкрементит writeback_queue_size без добавления якоря → осиротевший dirty-run никогда не флашится.

Ну в рантайме всё-таки не предполагается включение writeback. Но нужно подумать, может стоит предусмотреть на всякий случай.

UPD В итоге поправил, регресс тест добавил.

src/client/nbd_proxy.cpp:1117-1121 — NBD_CMD_DISC делает exit(0) без cli->flush() (в отличие от ветки EPOLLRDHUP), теряя writeback-кэш при штатном отключении клиента.

Это поправил.

Логика / CLI >src/cmd/cli_rm_data.cpp:64-69 — use-after-move: финальный SYNC после удаления никогда не отправляется Поправил >src/cmd/cli_merge.cpp:253-257 — continue_merge игнорирует ошибки валидации start_merge Тоже смешной баг, поправил >src/cmd/cli_rm.cpp:159-165 и :189-191 — порядок удаления в rm. (159) Inverse-child workflow уничтожает данные ребёнка до etcd-rename Тут как бы корректно, конкурентных операций одновременно с rm inverse-child не должно быть. Ну то есть логично, если ты решил что-то нехорошее сделать с парентом, пока его переименовываешь в чайлда, ничего хорошего не будет. При простых прерываниях тоже ничего плохого не будет - т.к. сначала мержится дочерний в родительский, потом удаляются данные дочернего, потом переименовывается родительский в дочерний. Писателей в этот момент быть не должно (механизма жёстко запретить это нет, но идея такая). >src/blockstore/v1/impl.cpp:439-474 — чанкованный online-решардинг оставляет clean_db_shards пустым для пула между паузами event-loop, пока флашер продолжает его мутировать → сброшенные обновления теряются/затеняются [требует подтверждения мейнтейнера]. ГМ, однако палево А как бы это и для нового стора тоже не было актуально... Надо поправить для обоих. **TODO** UPD Отключил этот chunked resharding вообще нафиг пока что. Нужно сначала поправить, тестов написать, потом включать обратно. >src/client/cluster_client_wb.cpp:127-134 — потерянный dirty-run при рантайм-включении writeback: расщепление CACHE_DIRTY-пробега не-DIRTY записью инкрементит writeback_queue_size без добавления якоря → осиротевший dirty-run никогда не флашится. Ну в рантайме всё-таки не предполагается включение writeback. Но нужно подумать, может стоит предусмотреть на всякий случай. UPD В итоге поправил, регресс тест добавил. >src/client/nbd_proxy.cpp:1117-1121 — NBD_CMD_DISC делает exit(0) без cli->flush() (в отличие от ветки EPOLLRDHUP), теряя writeback-кэш при штатном отключении клиента. Это поправил.
Owner

Так, отпишусь и потом коммент буду править, а то много всего

"Средние (краш/DoS/восстановимые рассинхроны) — кратко"

disk_tool_journal.cpp:510 - bounds-check читает len до инициализации записи → переполнение new_journal_buf на oversized-дампе

Поправил

blockstore_sync.cpp:108 - assert(!lsn) рушит OSD при ретрае do_sync после WAIT_SQE (assert включён в release)

Поправил

osd_primary_chain.cpp:64 - ретрай chained-read по EIO/EDOM падает на assert(op_data->st < base_state)

Поправил с регресс тестом )

blockstore_stable.cpp:55,70 - busy-loop / пропуск записи мета-блока при ENOSPC/CHECK_SQES в середине батча stabilize

Поправил путём удаления сомнительной логики, но без теста

blockstore_write.cpp:330 - big-write, завершившаяся во время in-flight fsync, идёт к записи метаданных без покрытия durability

Поправил с двумя регресс тестами! :) первый чтобы батчевание fsync правильно работало, второе чтобы не-батчевание fsync правильно работало.

blockstore_heap.cpp:2007 - underflow to_compact_count после рестарта (декремент при GC без инкремента при загрузке)

Поправил с тестом

blockstore_heap.cpp:666,1047,1766,2570 - iterator-invalidation/UAF и NULL-разыменования в recovery/compaction/reshard heap

Тут три штуки (1047, 1766 и 2570) неправда. Первое типа правда, но относится к воркараунду для проблемы с double-claim и поломать по факту что-то может в очень редких случаях, но в общем так или иначе поправил.

v1/read.cpp:367,493,688,100 - OOB-чтение в последнем IOV_MAX-батче; утечка dyn_data refcount; неосвобождение data-блока 0; чтение журнала без пиннинга против trim

  • OOB-чтение в последнем IOV_MAX-батче - вроде правда, вроде поправил, правда в жизни хрен это воспроизведёшь - IOV_MAX = 1024, значит, чтобы на это напороться, нужно поставить размер объекта как минимум 4 МБ и прочитать его фрагментами по 4 КБ через один... ну или поставить bitmap_granularity 512 байт и тогда можно размер объекта 512 кб...
  • утечка dyn_data refcount - вроде да, в нетривиальных конфигурациях, конкретно при наличии чексумм с 4 кб блоком чексуммы и либо !inmemory_metadata либо блоком данных больше 256 кб... исправил, добавил тест, попутно тем же тестом ещё баг поймал
  • неосвобождение data-блока 0 - уже напоролся сам при написании теста для другого бага от GPT-5-5 и поправил
  • чтение журнала без пиннинга против trim - UPDATE есть там эта защита, изначально неправильно написал. там трекаются used_sectors во время чтения

osd_peering_pg.cpp:162, osd_primary.cpp:508 - 32-битная маска 1 << role ломает EC с pg_size > 31 (UB-сдвиг) → healthy-объекты помечаются incomplete

Поправил чтобы она была 64-битной плюс просто запретил пока в osd старт пулов с pg_size > 64. Всё равно кому это нужно, pg_size 64.

osd_secondary.cpp:131, etcd_state_client.cpp:1208, osd.cpp:340, http_client.cpp:423 uncaught-исключения / 0ms-таймер / busy-spin на EAGAIN → abort или зависание event-loop (crash-loop на весь кластер)

  • что там в osd_secondary.cpp:131 - не понял, там просто if выбора типа операции.
  • uncaught-исключения - видимо имеется в виду std::stoull(), убрал, он правда в одном месте только использовался.
  • 0ms-таймер - видимо имеется в виду если поставить autosync_interval=0. исправил.
  • busy-spin на EAGAIN - это в http_client, типа не баг, это упрощение я специально делал, чтобы типа http_client не зависел от io_uring... хотя конечно это не нужно уже и можно поменять на io_uring.

osd_flush.cpp:95 - ошибка remote-flush навсегда подвешивает PG в PG_REPEERING

Поправил с регресс тестом, точнее снова двумя :) тут интересно то, что это идёт как followup к проблеме которую я в 3.0.13 классифицировал как опасный проезд по памяти, а по факту после написания теста уже не уверен, что там этот проезд мог быть. Возможно его и не могло быть...

Логика так или иначе, правда, была не очень корректная - так что с тестом теперь будет всё как надо. Теоретически это может быть даже исправлением древней редкой проблемы с зависанием EC PG в repeering.

ublk_server.cpp:739, nfs_proxy_rdma.cpp:589 разыменование get_sqe() без NULL-проверки (SIGSEGV при queue_depth≥1024); бесконечный цикл post_send

  • ublk_server.cpp:739 - не было обработки заполненного кольца. Ну это я знаю, да... :) Ну раз такое дело, добавил обработку...
  • nfs_proxy_rdma.cpp:589 - тут не понял о чём речь, что за бесконечный цикл

cli_dd.cpp:536,921 глотает ошибки fsync/SYNC (рапортует успех при сбое коммита); pipe-вход без count копирует один блок

  • сбой коммита на практике не бывает, но для чистоты кода добавил туда обработку ошибки
  • pipe-вход без count копирует один блок - неправда, проверил

cli_merge.cpp:641 UAF: SYNC-subop в delete_source не учтён в in_flight, snap_merger_t освобождается до колбэка

добавил in_flight++/--

cluster_client.cpp:349 UAF: continue_ops() разыменовывает освобождённый op при синхронном завершении следующего SYNC

когда смотришь свежим взглядом, замечание правда выглядит потенциально корректным - модель продолжения операций там как будто кривая, в том плане что связный список операций может модифицироваться и из continue_ops, и из всяких других мест, и выглядит, как будто должно быть можно подобрать ситуацию, когда это приведёт к UAF.

с другой стороны, ни клод, ни deepseek пока не осилили написать тест, который бы это реально воспроизводил. :-)

кроме того - вроде не было ни одного живого багрепорта, где падал бы клиент. а UAF к этому с большой вероятностью бы приводил...

ну и в целом, сходу переделывать это стрёмно, т.к. изменение получается немаленькое и можно наоборот что-то ещё им поломать.

так что править наверное пока не буду, но отложу на будущее, нужно там подразобраться и в порядок привести. при этом нужно не потерять текущие характеристики - в частности то, что при завершении операции нужно проходиться по следующим и продолжать их, если они от неё зависят, т.к. это влияет на перформанс - я эти связные списки туда внедрял ради перформанса.

UPD попробовал тоже воспроизвести, короче с SYNCами это точно нереально, т.к. это нужно чтобы колбэк завершения, причём успешного завершения, части операции вызвался внутри continue_ops, а это невозможно, он из eventloop (из мессенджера) вызывается...

может быть это как-то возможно с writeback-ами, но там сильно потрахаться надо, чтобы это воспроизвести. но на будущее прикопаем.

kv_db.cpp:236 32-битное переполнение в bounds-check read_string → ~4ГБ OOB-чтение на битом KV-блоке

поправил, хотя это типа только для обработки некорректных данных на диске

blockstore_disk.cpp:246, blockstore_init.cpp:101, v1/init.cpp:348, disk_tool_resize_auto.cpp:119 невалидированный meta_area_size/частичный блок → abort на старте; суперблок пишется до инициализации меты; underflow block_count-done_cnt; устаревший data_size после resize

  • невалидированный meta_area_size/частичный блок → abort на старте - не понял о чём это. abort на старте если что-то некорректно правильно звучит
  • суперблок пишется до инициализации меты - поправил уже от GPT-5-5
  • underflow block_count-done_cnt - неохота разбираться, добавил проверку на всякий случай, ну имеется в виду, если область меты в старом сторе сильно больше, чем размер данных
  • устаревший data_size после resize - думаю это там не проблема, это автоматический тул, там data_size явного в суперблоке и нет

nbd_proxy.cpp:623 --readonly парсится, но не применяется — NBD всегда RW

действительно, добавил обработку опции

node-binding/client.cc:61,1119-1120 UAF/NULL в Node.js-аддоне: uv_poll_t без uv_close; listing без Ref во время in-flight list_next

  • uv_poll_t без uv_close - хм ну да, я делал uv_poll_stop(), а наверное надо uv_close(). поменял. нет не поменял, close делает нода на своём event loop-е. а я только poll останавливаю, так что всё правильно было там.
  • listing без Ref - как же без Ref, когда прям в этой строке 1119 стоит Ref()

rdma_alloc.cpp:141 - re-insert фрагмента в freelist с адресом базы региона вместо адреса фрагмента

Написал тест на этот аллокатор, в итоге поправил условно или 2 или 3 бага, включая этот.

Так, отпишусь и потом коммент буду править, а то много всего "Средние (краш/DoS/восстановимые рассинхроны) — кратко" > disk_tool_journal.cpp:510 - bounds-check читает len до инициализации записи → переполнение new_journal_buf на oversized-дампе Поправил > blockstore_sync.cpp:108 - assert(!lsn) рушит OSD при ретрае do_sync после WAIT_SQE (assert включён в release) Поправил >osd_primary_chain.cpp:64 - ретрай chained-read по EIO/EDOM падает на assert(op_data->st < base_state) Поправил с регресс тестом ) > blockstore_stable.cpp:55,70 - busy-loop / пропуск записи мета-блока при ENOSPC/CHECK_SQES в середине батча stabilize Поправил путём удаления сомнительной логики, но без теста >blockstore_write.cpp:330 - big-write, завершившаяся во время in-flight fsync, идёт к записи метаданных без покрытия durability Поправил с двумя регресс тестами! :) первый чтобы батчевание fsync правильно работало, второе чтобы не-батчевание fsync правильно работало. >blockstore_heap.cpp:2007 - underflow to_compact_count после рестарта (декремент при GC без инкремента при загрузке) Поправил с тестом >blockstore_heap.cpp:666,1047,1766,2570 - iterator-invalidation/UAF и NULL-разыменования в recovery/compaction/reshard heap Тут три штуки (1047, 1766 и 2570) неправда. Первое типа правда, но относится к воркараунду для проблемы с double-claim и поломать по факту что-то может в очень редких случаях, но в общем так или иначе поправил. >v1/read.cpp:367,493,688,100 - OOB-чтение в последнем IOV_MAX-батче; утечка dyn_data refcount; неосвобождение data-блока 0; чтение журнала без пиннинга против trim - OOB-чтение в последнем IOV_MAX-батче - вроде правда, вроде поправил, правда в жизни хрен это воспроизведёшь - IOV_MAX = 1024, значит, чтобы на это напороться, нужно поставить размер объекта как минимум 4 МБ и прочитать его фрагментами по 4 КБ через один... ну или поставить bitmap_granularity 512 байт и тогда можно размер объекта 512 кб... - утечка dyn_data refcount - вроде да, в нетривиальных конфигурациях, конкретно при наличии чексумм с 4 кб блоком чексуммы и либо !inmemory_metadata либо блоком данных больше 256 кб... исправил, добавил тест, попутно тем же тестом ещё баг поймал - неосвобождение data-блока 0 - уже напоролся сам при написании теста для другого бага от GPT-5-5 и поправил - чтение журнала без пиннинга против trim - UPDATE есть там эта защита, изначально неправильно написал. там трекаются used_sectors во время чтения >osd_peering_pg.cpp:162, osd_primary.cpp:508 - 32-битная маска 1 << role ломает EC с pg_size > 31 (UB-сдвиг) → healthy-объекты помечаются incomplete Поправил чтобы она была 64-битной плюс просто запретил пока в osd старт пулов с pg_size > 64. Всё равно кому это нужно, pg_size 64. >osd_secondary.cpp:131, etcd_state_client.cpp:1208, osd.cpp:340, http_client.cpp:423 uncaught-исключения / 0ms-таймер / busy-spin на EAGAIN → abort или зависание event-loop (crash-loop на весь кластер) - что там в osd_secondary.cpp:131 - не понял, там просто if выбора типа операции. - uncaught-исключения - видимо имеется в виду std::stoull(), убрал, он правда в одном месте только использовался. - 0ms-таймер - видимо имеется в виду если поставить autosync_interval=0. исправил. - busy-spin на EAGAIN - это в http_client, типа не баг, это упрощение я специально делал, чтобы типа http_client не зависел от io_uring... хотя конечно это не нужно уже и можно поменять на io_uring. >osd_flush.cpp:95 - ошибка remote-flush навсегда подвешивает PG в PG_REPEERING Поправил с регресс тестом, точнее снова двумя :) тут интересно то, что это идёт как followup к проблеме которую я в 3.0.13 классифицировал как опасный проезд по памяти, а по факту после написания теста уже не уверен, что там этот проезд мог быть. Возможно его и не могло быть... Логика так или иначе, правда, была не очень корректная - так что с тестом теперь будет всё как надо. Теоретически это может быть даже исправлением древней редкой проблемы с зависанием EC PG в repeering. >ublk_server.cpp:739, nfs_proxy_rdma.cpp:589 разыменование get_sqe() без NULL-проверки (SIGSEGV при queue_depth≥1024); бесконечный цикл post_send - ublk_server.cpp:739 - не было обработки заполненного кольца. Ну это я знаю, да... :) Ну раз такое дело, добавил обработку... - nfs_proxy_rdma.cpp:589 - тут не понял о чём речь, что за бесконечный цикл >cli_dd.cpp:536,921 глотает ошибки fsync/SYNC (рапортует успех при сбое коммита); pipe-вход без count копирует один блок - сбой коммита на практике не бывает, но для чистоты кода добавил туда обработку ошибки - pipe-вход без count копирует один блок - неправда, проверил >cli_merge.cpp:641 UAF: SYNC-subop в delete_source не учтён в in_flight, snap_merger_t освобождается до колбэка добавил in_flight++/-- >cluster_client.cpp:349 UAF: continue_ops() разыменовывает освобождённый op при синхронном завершении следующего SYNC когда смотришь свежим взглядом, замечание правда выглядит потенциально корректным - модель продолжения операций там как будто кривая, в том плане что связный список операций может модифицироваться и из continue_ops, и из всяких других мест, и выглядит, как будто должно быть можно подобрать ситуацию, когда это приведёт к UAF. с другой стороны, ни клод, ни deepseek пока не осилили написать тест, который бы это реально воспроизводил. :-) кроме того - вроде не было ни одного живого багрепорта, где падал бы клиент. а UAF к этому с большой вероятностью бы приводил... ну и в целом, сходу переделывать это стрёмно, т.к. изменение получается немаленькое и можно наоборот что-то ещё им поломать. так что править наверное пока не буду, но отложу на будущее, нужно там подразобраться и в порядок привести. при этом нужно не потерять текущие характеристики - в частности то, что при завершении операции нужно проходиться по следующим и продолжать их, если они от неё зависят, т.к. это влияет на перформанс - я эти связные списки туда внедрял ради перформанса. UPD попробовал тоже воспроизвести, короче с SYNCами это точно нереально, т.к. это нужно чтобы колбэк завершения, причём успешного завершения, части операции вызвался внутри continue_ops, а это невозможно, он из eventloop (из мессенджера) вызывается... может быть это как-то возможно с writeback-ами, но там сильно потрахаться надо, чтобы это воспроизвести. но на будущее прикопаем. >kv_db.cpp:236 32-битное переполнение в bounds-check read_string → ~4ГБ OOB-чтение на битом KV-блоке поправил, хотя это типа только для обработки некорректных данных на диске >blockstore_disk.cpp:246, blockstore_init.cpp:101, v1/init.cpp:348, disk_tool_resize_auto.cpp:119 невалидированный meta_area_size/частичный блок → abort на старте; суперблок пишется до инициализации меты; underflow block_count-done_cnt; устаревший data_size после resize - невалидированный meta_area_size/частичный блок → abort на старте - не понял о чём это. abort на старте если что-то некорректно правильно звучит - суперблок пишется до инициализации меты - поправил уже от GPT-5-5 - underflow block_count-done_cnt - неохота разбираться, добавил проверку на всякий случай, ну имеется в виду, если область меты в старом сторе сильно больше, чем размер данных - устаревший data_size после resize - думаю это там не проблема, это автоматический тул, там data_size явного в суперблоке и нет >nbd_proxy.cpp:623 --readonly парсится, но не применяется — NBD всегда RW действительно, добавил обработку опции >node-binding/client.cc:61,1119-1120 UAF/NULL в Node.js-аддоне: uv_poll_t без uv_close; listing без Ref во время in-flight list_next - uv_poll_t без uv_close - ~~хм ну да, я делал uv_poll_stop(), а наверное надо uv_close(). поменял.~~ нет не поменял, close делает нода на своём event loop-е. а я только poll останавливаю, так что всё правильно было там. - listing без Ref - как же без Ref, когда прям в этой строке 1119 стоит Ref() >rdma_alloc.cpp:141 - re-insert фрагмента в freelist с адресом базы региона вместо адреса фрагмента Написал тест на этот аллокатор, в итоге поправил условно или 2 или 3 бага, включая этот.
Sign in to join this conversation.
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vitalif/vitastor#75
No description provided.