Memory/logic vulnerabilities review using Opus 4.8 w/ ultracode #74
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 (SDS) — отчёт
Сводка
nfs/proto/xdr_impl_inline.h:94-97nfs/proto/xdr_impl_inline.h:318-333nfs/nfs_portmap.cpp:134nfs/nfs_kv_write.cpp:430-439blockstore/blockstore_disk.cpp:529-557osd/osd_rmw.cpp:1190-1219,1341disk_tool/disk_tool_meta.cpp:929-937blockstore/v1/read.cpp:175,246-247blockstore/blockstore_heap.cpp:560-569&&вместо||blockstore/blockstore_impl.cpp:348client/cluster_client.cpp:347-369client/etcd_state_client.cpp:1240-1255client/msgr_send.cpp:300-305osd/osd_primary_chain.cpp:52-64,85osd/osd_scrub.cpp:630-635osd/osd_peering_pg.cpp:162-164disk_tool/disk_tool_journal.cpp:507-535kv/kv_db.cpp:234-243util/rw_blocking.cpp:60-93blockstore/v1/read.cpp:364-370blockstore/v1/read.cpp:682-689,947-963osd/osd_primary_write.cpp:247-251osd/osd_peering.cpp:421-457client/nbd_proxy.cpp:74-78client/ublk_server.cpp:717-744util/rw_blocking.cpp:18-31kv/kv_db.cpp:601-618disk_tool/disk_tool_utils.cpp:36-45FP-1, FP-2 — см. раздел «Отклонённые false-positive».
Категория B — провоцируются внешним/удалённым вводом
B-1 ? Удалённый OOB-read (~4 ГБ): переполнение в
len_pad4()обходит проверку границ XDR-строки/opaquesrc/nfs/proto/xdr_impl_inline.h:94-97, 132, 142-1480xFFFFFFFF)Механизм. В
xdr_bytes()(декодирование) 32-битная длина с провода читается вlenи передаётся вlen_pad4():Для
len ∈ {0xFFFFFFFD, 0xFFFFFFFE, 0xFFFFFFFF}выражениеlen+3переполняется в{0,1,2}, поэтомуpadded == 0. Проверкаavail < 4+0проходит для любого буфера ≥4 байт, иdata->sizeустанавливается вlen(~4 ГБ). Параметрmaxlenна декодировании не проверяется — ничто не ограничивает длину.Триггер. Один поддельный NFS/portmap-RPC-запрос с полем строки/opaque (имя в LOOKUP, путь монтирования, file handle, любое
xdr_string/xdr_bytes-поле) с префиксом длины0xFFFFFFFF. Любой потребитель (xdr_string_t::operator std::string()→std::string(data, size),operator==) читает ~4 ГБ за пределами маленького приёмного буфера.Последствия. OOB-чтение → падение демона NFS (DoS) или утечка соседней heap-памяти. Без аутентификации, удалённо.
Исправление. Проверять переполнение до округления:
if (len > xdrs->avail - 4) return 0;(в 64-битной арифметике) и применятьmaxlenна декодировании. Это системная проблема — чинить централизованно.B-2 ? Удалённое heap-переполнение: переполнение счётчика элементов в
xdr_array()→ undersized mallocsrc/nfs/proto/xdr_impl_inline.h:318-333Механизм.
Произведение
entry_size * (*len)считается в 32-битной беззнаковой арифметике и переполняется. Атакующий подбирает*lenтак, чтобы произведение свернулось в малое значение, а сам*lenостался огромным. Пример:SETACL3args.ace[],entry_size = sizeof(nfsacl_ace) = 12,*len = 0x55555556→12*0x55555556 mod 2^32 = 8байт. Выделяется 8 байт, а цикл пишет 12-байтные записи далеко за их пределы.Триггер. Поддельный NFSv3 SETACL (
maxlen=~0,entry_size=12); сотни ACE-записей в пакете → переполнение на тысячи управляемых атакующим байт. Аналогично черезxdr_xdr_write_chunk(RDMA,entry_size=16,maxlen=~0) и массивы GETACL/auth_flavors.Последствия. Управляемое атакующим heap-переполнение → порча памяти / удалённый крах демона. Потенциально эксплуатируемо.
Исправление. Проверять
if (*len != 0 && entry_size > SIZE_MAX / *len) return 0;в 64-битной арифметике доmalloc, и проверять, что*lenпомещается в оставшийся ввод.B-3 ? Удалённое heap-переполнение в PMAP3 DUMP: выделяются слоты под указатель, пишутся полные структуры
src/nfs/nfs_portmap.cpp:134Механизм.
Выделяется
sizeof(указатель)= 8 байт на запись, но пишется полнаяpmap3_mapping_list(~64 байта:pmap3_mapping= 2×u_int + 3×xdr_string_t{size_t,char*} = 56 байт + указательnext). Соседнийpmap2_dump_proc(строка 67) корректно используетsizeof(pmap2_mapping_list)— это опечатка, лишняя*. При 4 всегда регистрируемых сервисах (PMAP_V2, PMAP_V3, NFS_V3, MOUNT_V3): выделяется 32 байта, пишется 4×64 = 256 байт +list[i-1].next = NULL.Триггер. Любой клиент, шлющий PMAP v3 DUMP на порт 111 — в т.ч.
rpcinfoи стандартный rpcbind-зонд, который Linux NFS-клиенты отправляют при монтировании. Переполнение происходит при любом dump-запросе, включая легитимные.Последствия. Удалённо провоцируемое heap-переполнение в демоне NFS-прокси → порча соседней heap-памяти, крах демона (DoS для всех NFS-клиентов), потенциальная эксплуатация.
Исправление. Убрать
*:malloc_or_die(sizeof(pmap3_mapping_list) * self->reg_ports.size()).B-4 ? Неверная длина зануления хвоста в NFS shared-write
src/nfs/nfs_kv_write.cpp:430-439Механизм. В
nfs_do_shared_writeхвост после новых данных в веткеhas_old==falseзаполняетсяadd_zero(op, st->offset, ...)— это copy-paste из ведущей области (строки 415-424). Должно бытьst->new_size-(st->offset+st->size)(как в соседней веткеhas_old):В iov попадает меньше байт, чем
op->len(=aligned_size) →assert(left == 0)вcluster_client_t::add_iov(src/client/cluster_client.cpp:1232).Триггер. Создать файл через CREATE/MKNOD UNCHECKED с атрибутом размера
N>0(nfs_kv_create.cpp:320-324→empty=true,size=N), затем маленький WRITE, гдеoffset+size+header < shared_inode_threshold.resume_1ставитnew_size=N, срабатывает перевод в shared-путь,nfs_do_shared_readmodifyничего не делает (shared_ino==0,aligned_buf==NULL), иnfs_do_shared_writeидёт сhas_old==falseиoffset+size<new_size.Последствия. Падение демона NFS-прокси по assert (DoS). С отключёнными assert — короткая запись iov → запись неверной длины/мусора в shared-inode → тихая порча данных файла.
Исправление. В else-ветке использовать
add_zero(op, st->new_size-(st->offset+st->size), ...).Категория A — проявляются сами по себе
A-1 ?
trim_data:continueпропускаетj = i+1→ BLKDISCARD по живым (используемым) блокам данныхsrc/blockstore/blockstore_disk.cpp:529-557Механизм. Параметр-предикат
trim_dataназываетсяis_free, но все вызывающие передают предикат is-used (blockstore_impl.cpp:93heap->is_data_used(...);disk_tool_discard.cpp:97,v1/impl.cpp:97data_alloc->get(...)) — он возвращает true для ЗАНЯТОГО блока. Когдаdiscard_granularityделает выровненный диапазон пустым (range[0] >= range[1]), выполняетсяcontinue, который прыгает к инкременту цикла и пропускаетj = i+1(строка 556). Занятый блокiне исключается,jпродолжает указывать в раннюю свободную область. На следующей границеi2вычисляется диапазон[j, i2), охватывающий занятый блок, и по нему выдаётсяBLKDISCARD.Триггер.
discard_granularityбольше/невыровнен относительно квалифицирующего свободного прогона. Путь выполняется на живом OSD при старте сdiscard_on_start=true(blockstore_impl.cpp:91-93) и вvitastor-disk discard.Последствия.
BLKDISCARDпо диапазону с занятыми блоками безвозвратно зануляет данные клиента → тихая потеря/порча данных тома.Исправление. Выполнять
j = i+1всегда (вынести из-подcontinue), либо заменитьcontinueна присваиваниеjперед ним.A-2 ? Heap-переполнение в
ec_check_combination: буфер размером поstripe_count, индексация поrolesrc/osd/osd_rmw.cpp:1190-1219, 1341Механизм.
ec_find_good()выделяетtmp_buf = malloc_or_die(stripe_count*(chunk_size+bitmap_size)), гдеstripe_count— число реально прочитанных чанков.ec_check_combination()индексирует по РОЛИi ∈ [0, pg_size):При
stripe_count < pg_sizeролиi ∈ [stripe_count, pg_size)дают указатели за пределами выделенной области, аreconstruct_stripes_ec()/calc_rmw_parity_ec()пишут туда полныйchunk_sizeданных иbitmap_size.Триггер. Scrub деградированного EC-объекта.
submit_scrub_subopsстроит массив stripes изobject_state->osd_set— по одной записи на присутствующий чанк, так чтоstripe_count = число присутствующих < pg_size.ec_find_goodвходит в перебор приlive_roles > pg_minsize. Напр. EC 3+2 (pg_size=5,pg_minsize=3) с одним отсутствующим чанком: 4 живые роли > 3 → перебор сstripe_count=4 < pg_size=5.Последствия. Запись полного чанка (обычно 128 КБ) + bitmap за концом heap-аллокации при scrub любого деградированного EC-объекта. Деградированные PG — штатное состояние, scrub автоматический → достижимый крах / порча памяти OSD.
Исправление. Выделять буфер по
pg_size, а неstripe_count:malloc_or_die(pg_size*(chunk_size+bitmap_size))и согласовать смещение bitmap-региона.A-3 ? Underflow беззнакового счётчика в обработке rollback при write-meta → бесконечный цикл и OOB read
src/disk_tool/disk_tool_meta.cpp:929-937Механизм.
iбеззнаковый,i >= 0всегда истинно. (1) Пустойjbo→jbo.size()-1 == SIZE_MAX→jbo[SIZE_MAX]сразу. (2) Непустой — послеi==0i--сворачивается вSIZE_MAX→jbo[SIZE_MAX]["version"]. Цикл не завершается нормально.Триггер.
vitastor-disk write-metaпри конвертации старого формата (v1/v2) metadata+journal JSON-дампа в heap-формат, если журнал содержит хоть одну запись типа"rollback".Последствия. Крах (SIGSEGV) / UB с чтением далеко за границами вектора во время конвертации метаданных оператором; инструмент не может завершиться, при повреждённом индексе может записать мусорные метаданные перед крахом.
Исправление.
for (size_t i = jbo.size(); i-- > 0; )либо знаковый индекс.A-4 ? Wild memset через NULL
read_bufвfill_partial_checksum_blocksиз flushersrc/blockstore/v1/read.cpp:175, 246-247(вызов изv1/flush.cpp:1089-1092)Механизм.
scan_dirty()вызываетfill_partial_checksum_blocks()сread_buf = NULL, но функция безусловно делаетread_buf -= read_offset;(read.cpp:175), гдеread_offset = v[0].offset, округлённый вниз доcsum_block_size, > 0 если первый перезаписываемый блок не на смещении 0. NULL превращается в(uint8_t*)(0 - read_offset). Дальшеread_range_fulfilled()делаетif (read_buf) memset(read_buf + el.offset, 0, el.len);— guard на NULL уже не срабатывает, и memset пишет нули по адресу около нуля.Триггер. Flush частично перезаписанного checksum-объекта (csum_block_size > bitmap_granularity, запись не выровнена по csum-блоку, первый патчируемый блок на offset>0, старый объект имеет дыры).
Последствия. Запись нулей по адресу около NULL → SIGSEGV в основном пути flush; если адрес замаплен — тихая порча памяти. Перемежающийся крах storage-демона.
Исправление. Не вычитать
read_offset, еслиread_buf == NULL; держать NULL до самого memset-guard.A-5 ?
finish_load: однаinsert_list_itemsдля разных объектов (&&вместо||)src/blockstore/blockstore_heap.cpp:560-569Механизм.
finish_load()сортируетpostponed_itemsпо (inode ASC, stripe ASC, lsn DESC) и должен нарезать на пакеты по одному объекту.insert_list_items()разрешает голову списка ровно один раз изv[0](inode_map_get(..., wr->stripe)) и сливает все остальные элементы в эту цепочку только по LSN (is_before). Значит, пакет ОБЯЗАН содержать один объект (одинаковые inode И stripe). Но граница задана так:По приоритету (
!=>&&>||) этоe>=n || (inode_differs && stripe_differs)— новый пакет начинается, только когда РАЗЛИЧАЮТСЯ ОБА. Для частого случая «один inode (образ) владеет многими stripe-объектами» разбиения нет → записи разных stripe сливаются в первую цепочку, а головы остальных объектов не регистрируются в inode-map.Триггер. Старт/загрузка metadata-стора, где ≥2 объекта накопили глубокую историю версий (
postponeсрабатывает при пропуске >POSTPONE_INSERT_COUNT=10 записей того же объекта — норма при тяжёлой перезаписи до компакции).Последствия. Порча in-memory индекса объектов при старте: записи одного объекта вклеиваются в цепочку другого, головы части объектов не попадают в inode-map.
read_entry()/list_objects()возвращают версии/локации чужого объекта (тихая порча данных) или NULL для существующих (потеря данных); кривые цепочки роняютassert()вunlink_list_item/remove_list_item/mark_garbage.Исправление. Заменить
&&на||: новый пакет, когда различается inode ИЛИ stripe.A-6 ?
process_list:memmoveиспользует число элементов как число байт (в 24 раза меньше)src/blockstore/blockstore_impl.cpp:348Механизм.
result—obj_ver_id*,sizeof(obj_ver_id)=24(object_id{inode,stripe}=16 + version=8). Указатели масштабируются арифметикой, но размерmemmove—unstable_count(число элементов) вместоunstable_count * sizeof(obj_ver_id). Копируется ~1/24 данных, остальные unstable-слоты сохраняют мусор от отброшенных stable-записей.retval = limit + unstable_count, поэтому вызывающий читает мусор как unstable-объекты.Триггер.
BS_OP_LISTсlist_stable_limit(scrub/recovery), когда stable-объектов в PG больше лимита И есть unstable-объекты.Последствия. Scrub/recovery работает по неверным (inode,stripe,version) → неверные решения о целостности → возможна порча данных.
Исправление.
memmove(..., unstable_count * sizeof(obj_ver_id));A-7 ? Use-after-free в
continue_ops(): закэшированныйnext_opможет быть освобождён реентрантным завершениемsrc/client/cluster_client.cpp:347-369Механизм.
continue_ops()кэшируетnext_op = op->nextдо прогонаopчерезcontinue_rw()/continue_sync(), затем безусловноop = next_opи перечитываетop->next. Прогонopможет синхронно завершить и освободитьnext_op.Сценарий: WRITE (не immediate-commit) + следующий за ним SYNC, ждущий WRITE. WRITE ретраится по таймеру; на ретрае
try_send()→TRY_SEND_OFFLINE, истёкOSD_OP_WAIT_UP_TIMEOUT→-ETIMEDOUT→erase_op(WRITE)→inc_wait→continue_sync(SYNC);dirty_osdsпуст → SYNC стирается, его callback удаляетcluster_op_t. Возврат в цикл →op = next_opуказывает на освобождённый SYNC. Защитаcontinuing_ops == 2мертва (присваивание убрано в коммитеf8ff39b0).Последствия. UAF
cluster_op_tв I/O-пути клиента: разыменование освобождённой операции, возможный повторный прогон уже завершённого SYNC → крах клиента (QEMU/NBD/fio/vitastor-cli) или двойной callback.Исправление. Восстановить корректную перезагрузку обхода (
continuing_ops=2/goto restart) после реентрантного завершения, либо проверять валидность передop = next_op.A-8 ? Разыменование end()-итератора при дублирующихся именах inode
src/client/etcd_state_client.cpp:1240-1255Механизм.
Инвариант «
inode_config[x].name != ""⇒inode_by_nameсодержит это имя» нарушается при двух inode с одинаковым именем. Последовательность: A с именем "foo" → B с именем "foo" (перезаписываетinode_by_name["foo"]=B, ноinode_config[A].nameостаётся "foo") → удаление B (стираетinode_by_name["foo"]) → удаление/обновление A:find("foo")возвращаетend(),n_it->secondразыменовываетend().Триггер. Дубль имён образов в etcd (переходно при rename или из-за мисконфигурации) + определённый порядок удаления/обновления.
Последствия. SIGSEGV/UB в любом демоне (клиент QEMU/fio, OSD), читающем конфигурацию inode из etcd → кластерный простой I/O.
Исправление. Проверять
n_it != inode_by_name.end()перед разыменованием.A-9 ?
zc_free_list: NULL-маркер не удаляется для OUT-only zerocopy-отправок → неограниченный рост памятиsrc/client/msgr_send.cpp:300-305Механизм. На каждую zerocopy-отправку приходят два завершения (more=true, затем prev=true F_NOTIF). На первом безусловно добавляется NULL-маркер в
zc_free_list. На втором батч освобождается:MSGR_SENDP_FREEставится только дляOSD_OP_IN-ответов;OSD_OP_OUTего не несёт. Для батча только из OUT-операций список =[NULL],iостаётся 0,if (i > 0)ложно — маркер не удаляется. Каждая такая отправка добавляет один NULL навсегда.Триггер. Исходящие OSD↔OSD соединения шлют репликационные записи (
OSD_OP_SEC_WRITE/_STABLE, все OUT). Большие записи (≥min_zerocopy_send_size, дефолт 32 КБ) идут zerocopy-путём при поддержкеsendmsg_zc.Последствия. На каждую большую zerocopy-запись
zc_free_listрастёт на один элемент и не сжимается → неограниченная утечка → OOM на write-heavy OSD за длительное аптайм.Исправление. Удалять одиночный NULL-маркер и при
i == 0(стирать[begin, begin+1)).A-10 ? Ретрай chained-read на EIO/EDOM оставляет
op_data->stустаревшим → assert-abortsrc/osd/osd_primary_chain.cpp:52-64, 85Механизм. При ошибке чтения/контрольной суммы на subop chained-read
op_data->stдоходит до 4. На-EIO/-EDOMкод делаетcheck_corrupted_chained(), освобождает буферы иgoto resume_1без сбросаop_data->st(он всё ещё 4). Вread_bitmaps()stне равен ниbase_state(1), ниbase_state+1(2) →assert(op_data->st < base_state)=assert(4 < 1)→ abort. Сборка релиза снимает-DNDEBUG(src/CMakeLists.txt:43-51), assert живой в продакшене.Триггер. Один битый сектор (
-EIO) или несовпадение контрольной суммы (-EDOM) при chained-read снапшота/клона (inode сparent_id).Последствия. Крах primary OSD по assert вместо ошибки/реконструкции. Объект остаётся повреждённым → повторные клиентские чтения снова роняют OSD (crash-loop) → недоступность всех PG этого OSD.
Исправление. Перед
goto resume_1выставлятьop_data->st = 0.A-11 ? EC-scrub индексирует
stripes[]поchunk.roleвместо индекса stripe → OOB read + порча метаданныхsrc/osd/osd_scrub.cpp:630-635Механизм.
op_data->stripes[]имеет размерstripe_count= число НЕ-outdated чанков, заполняется последовательно (stripes[i].role = chunk.role), индекс не связан с номером роли. Но код обращается кop_data->stripes[chunk.role], гдеchunk.role— доpg_size-1(до 255 для EC):При наличии outdated-чанка
stripe_count < число ролей→chunk.role >= stripe_count→ OOB-чтение heap-массива; даже в границах читается.missingчужого stripe.Триггер. Scrub EC/XOR-объекта с outdated-копией (
stripe_countуменьшен) + чанк, ранее помеченныйLOC_CORRUPTED, чья роль ≥stripe_count, и который теперь читается корректно.Последствия. OOB-чтение (крах на границе страницы) и чтение мусорного
.missing→ неверное решение о снятииLOC_CORRUPTED→ primary может читать действительно битый чанк (выдача неверных данных) или отбросить хороший.Исправление. Использовать
op_data->stripes[i].missingвместо[chunk.role].A-12 ? Маска
n_roles: 32-битный1 << replicaдля EC-ролей до 255 → UB сдвигаsrc/osd/osd_peering_pg.cpp:162-164Механизм.
1— знаковыйint,replicaдоpg_size-1(EC допускает до 256,etcd_state_client.cpp:868).1 << replicaприreplica >= 31— UB (сдвиг ≥ ширины/в знаковый бит); даже с(uint64_t)1роли ≥64 не представимы.n_roles(число присутствующих чанков) считается неверно приpg_size > 31.Триггер. Любой EC-пул с
pg_size > 31(напр. 30+3) на peering/расчёте состояний объектов.Последствия. На широких EC-пулах решение complete/degraded/incomplete считается по испорченной маске → завершённые объекты помечаются incomplete (отказ чтений/записей, зависание I/O) или деградированные — complete (чтение по недостаточному набору чанков). Плюс формальный UB.
Исправление.
(uint64_t)1 << replicaи хранилище шире 64 бит (напр.std::bitset/массив) приpg_size > 64.A-13 ?
write_json_journal: проверка границ читаетsmall_write.lenдо присваивания → heap overflowsrc/disk_tool/disk_tool_journal.cpp:507-535Механизм.
neуказывает в обнулённыйnew_journal_buf. Проверкавыполняется ДО заполнения структуры (
*((journal_entry_small_write*)ne) = {... .len = rec["len"] ...}идёт позже, строки 516-530). Поэтомуne->small_write.lenчитается как 0 → проверка всегда проходит. Затемfromhexstr(rec["data"]..., ne->small_write.len, new_journal_data)пишет реальныеlenбайт, а guard, который должен был это поймать, считался против 0.Триггер.
vitastor-disk write-journalиз JSON-дампа, где суммарные данные small-write превышаютnew_journal_len.Последствия. Heap-overflow при реконструкции журнала; защита-no-op → порча соседней памяти, крах или запись повреждённого журнала на диск.
Исправление. Вычислять длину из
rec["len"]в локальную переменную и проверять её до записи.A-14 ? Integer overflow в
read_string()→ OOB readsrc/kv/kv_db.cpp:234-243Механизм.
*pos—int,len—uint32_t: сложение в 32-битной беззнаковой арифметике. Приlen ≈ 0xFFFFFFFF*pos+lenсворачивается в малое значение → проверка проходит →std::string(data+pos, ~4 ГБ)OOB read.Триггер. Повреждённый/torn KV-блок с целыми заголовками (magic/block_size/type/items), но испорченным полем длины ключа/значения. Достижимо из
get_block,find_size,rescue.Последствия. Многогигабайтное OOB-чтение в
kv_block_t::parse()→ SIGSEGV вместо чистого-EILSEQ. Восстановимое состояние битого блока превращается в крах демона.Исправление. Проверять
if (len > (uint32_t)(size - *pos)) ...либо в 64-битной арифметике.A-15 ?
readv_blocking/writev_blocking/sendv_blocking: бесконечный цикл на EOFsrc/util/rw_blocking.cpp:60-93Механизм. Ошибкой считается только
r < 0.r == 0(peer закрыл соединение/EOF до доставки всех байт iov) не обрабатывается:done += 0, внутренний цикл попадает на первый iov сiov_len > 0, берёт веткуiov_len > r(т.е. > 0), вычитает 0, сдвигает базу на 0 иbreakБЕЗ инкрементаv. Внешнийwhile (v < iovcnt)истинен →readv()снова возвращает 0 → 100% CPU навсегда.Триггер.
fio_sec_osd.cpp:377readv_blocking(connect_fd, ...)читает payload ответа OSD; если OSD упал/закрыл соединение после заголовка — следующийreadvвозвращает 0.Последствия. Отключение удалённого пира превращается в неубиваемый busy-loop (зависание на 100% CPU) вместо чистой ошибки.
Исправление. Обрабатывать
r == 0как EOF (возврат/ошибка), а не как «продолжить».A-16 ? OOB read iovec-массива в
read_checksum_block(батчинг IOV_MAX)src/blockstore/v1/read.cpp:364-370Механизм. Выделяется массив ровно из
n_ioviovec, подаётся батчами поIOV_MAX. Для каждого батча (кроме одиночного первого чанка) сумма длин считаетсяfor (int i = 0; i < IOV_MAX; i++) d_len += iov[n_pos+i].iov_len;. На последнем батче, когдаn_iov > IOV_MAXи не кратно, реальных записейn_cur = n_iov-n_pos < IOV_MAX→ читаются индексы>= n_iovза концом буфера.Триггер. Очень крупные/фрагментированные checksum-block-чтения, дающие > IOV_MAX (обычно 1024) сегментов.
Последствия. Heap OOB-чтение за аллокацией + неверный
data->iov.iov_len→handle_read_eventложно помечает чтение ошибкой → ложный short-read/IO-error клиенту.Исправление. Ограничить внутренний цикл
min(IOV_MAX, n_iov - n_pos).A-17 ? Ссылка
used_clean_objectsне освобождается для блока данных в локации 0src/blockstore/v1/read.cpp:682-689, 947-963Механизм.
fulfill_clean_read()сохраняетPRIV(read_op)->clean_block_used = clean_loc. Для первого блока данныхclean_loc == 0:used_clean_objects[0]создаётся,uo.refs++. Путь освобождения вhandle_read_eventзащищёнif (PRIV(op)->clean_block_used)— ложно при локации 0, refcount не уменьшается, запись не удаляется.Триггер. Любое чтение, затрагивающее блок данных в локации 0.
Последствия. Постоянная (refs>0) запись в
used_clean_objects. Если flusher позже релоцирует/освободит блок (was_freed=true), он не возвращается аллокатору → теряется один блок из free-пула + накопление stale-состояния.Исправление. Использовать sentinel (напр.
UINT64_MAX) для «нет ссылки» вместо 0, либо отдельный bool-флаг.A-18 ? Утечка массива write-subops при частичном сбое EC-записи (ENOSPC)
src/osd/osd_primary_write.cpp:247-251Механизм. При частичном сбое записи (часть subops успешна) код намеренно НЕ удаляет
op_data->subops(osd_primary_subops.cpp:491-497). Для реплик:mark_partial_write+delete[] op_data->subops. Для EC/XOR:submit_primary_rollback_subops()делаетop_data->subops = new osd_op_t[n_subops];, перезаписывая ещё живой указатель безdelete[]. Rollback-массив позже освобождается, исходный — утекает.Триггер. EC/XOR-запись, частично упавшая с ENOSPC (диск полон на части чанк-OSD).
Последствия. Утечка одного
new osd_op_t[n_subops]на каждую такую запись. При disk-full клиент/recovery повторяют записи → утечка растёт именно при дефиците памяти.Исправление. Освобождать исходный
op_data->subopsперед переназначением в EC-ветке.A-19 ?
relock_pgстирает relock-записи изdiff_osdsбез коррекцииrelock_osd_countsrc/osd/osd_peering.cpp:421-457Механизм.
diff_osds= [relock-записи (первыеrelock_osd_count)] ++ [unlock-записи]. Классификация:unlock_peer = (i >= relock_osd_count). Когда пир из relock-части подключён, но не поддерживает PG-locks, запись стираетсяdiff_osds.erase(begin()+(i--))без декрементаrelock_osd_count. Все последующие unlock-записи сдвигаются влево → реальная unlock-запись оказывается на индексе< relock_osd_count→ классифицируется как relock.Триггер. Смешанный по версиям кластер (rolling upgrade), где хотя бы один cur_peer OSD без фичи
pg_locks, при наличии пиров на unlock.Последствия. Пир, который надо разблокировать, записывается в
pg.lock_peersс ненулевым state.can_stop()возвращает false → PG застревает вPG_STOPPING, блокируя миграцию/takeover.Исправление. Декрементировать
relock_osd_countпри стирании записи из relock-части.A-20 ? netlink-сокет освобождается через
free()вместоnl_socket_free()src/client/nbd_proxy.cpp:74-78Механизм.
netlink_sock_free()освобождаетstruct nl_sock*(отnl_socket_alloc()) через libcfree()вместо libnlnl_socket_free().nl_socket_alloc()открывает netlink-fd (черезgenl_connect→socket()) и refcountednl_cb.free()освобождает только структуруnl_sock, оставляя fd открытым иnl_cbутёкшим. (В том же файле строки 59, 66 в путях ошибок корректно вызываютnl_socket_free.)Триггер. Каждая операция netlink-map/unmap/revive (
netlink_configure(),netlink_disconnect()).Последствия. Каждый netlink-map утекает netlink-fd +
nl_cb. Для долгоживущего прокси fd держится всё время жизни устройства; повторные map/unmap исчерпывают таблицу дескрипторов.Исправление.
nl_socket_free(sk)вместоfree(sk).A-21 ? Непроверенный
get_sqe()→ NULL-deref приqueue_depthбольше размера io_uring-кольцаsrc/client/ublk_server.cpp:717-744Механизм.
start_device()пред-сабмититqueue_depthfetch-запросов в цикле, каждыйsubmit_request()→ringloop->get_sqe(), аsubmit()вызывается ПОСЛЕ цикла — значит всеqueue_depthSQE должны быть живы одновременно. Кольцо создаётся с фиксированнымRINGLOOP_DEFAULT_SIZE(1024).get_sqe()возвращает NULL при исчерпании.submit_request()не проверяет результат:sqe->fd = cdev_fd;,((ring_data_t*)sqe->user_data).Триггер. Маппинг ublk-устройства с
--queue_depth > 1024(документированная опция; типично для высокопроизводительных NVMe-class устройств).Последствия. Крах
vitastor-ublkпо NULL-deref (SIGSEGV) при старте, сразу после добавления kernel-устройства → полуинициализированный/dev/ublkbN, отказ в обслуживании при валидной конфигурации.Исправление. Проверять
get_sqe()на NULL (сабмитить и ждать освобождения SQE), либо создавать кольцо размером ≥queue_depth.A-22 ?
read_blocking: определение EOF по устаревшемуerrno→ abort процесса или зависаниеsrc/util/rw_blocking.cpp:18-31Механизм.
read() == 0(EOF) не меняетerrno.read_blockingвходит в веткуr <= 0и решает про EOF поif (!errno).errnoхранит произвольное значение от прошлого syscall. Если 0 — вернёт short count; если EINTR/EAGAIN/EPIPE —continue→ снова 0 → бесконечный цикл; иначе —perror("read"); exit(1). (Ср.read_all_fd()вstr_util.cpp, корректно проверяющий!r.)Триггер.
disk_tool_meta.cppчитает метаданныеread_blocking(meta_fd, hdr, meta_block_size); на усечённом/битом образе read возвращает частичный счётчик, затем 0.Последствия. Short-read на EOF недетерминированно роняет disk_tool/клиент
exit(1)или зацикливает, в зависимости от постороннего значенияerrno.Исправление. Сначала проверять счётчик байт (
if (r == 0) → EOF), а неerrno.A-23 ? OOB read в цикле rescue, когда
kv_block_sizeне делит 1 МиБ-буферsrc/kv/kv_db.cpp:601-618Механизм.
kv_rescue_tчитает индекс чанками 1 МиБ (1048576 = 2²⁰), итерируетblk.parse(pos+cur_offset, buf.data()+cur_offset, kv_block_size, true)покаcur_offset < cur_size, шаг+= kv_block_size. На полном чанке (cur_size == 1048576), еслиkv_block_sizeне делит 2²⁰, наибольшее кратное< 1048576ещё проходитcur_offset < cur_size, ноcur_offset + kv_block_size > 1048576→parse()читает за концом буфера.open()требует лишь, чтобыkv_block_sizeделилino_block_size(data_block_size*pg_data_size), но не 1048576. EC-пул (напр. pg_data=3, data_block_size=128K → ino_block_size=393216) допускаетkv_block_size=12288, не делящий 1048576.Триггер.
rescue()БД сkv_block_size, не делящим 1048576 (легально при EC).Последствия. OOB-чтение за heap-буфером на каждом полном чанке во время disaster recovery → крах (ASan) или мусор, разобранный как блок.
Исправление. Ограничивать
parse()длинойmin(kv_block_size, cur_size - cur_offset), либо требовать делимость 1048576.A-24 ?
fromhexstrчитает за пределами исходной строки, если hex короче2*bytessrc/disk_tool/disk_tool_utils.cpp:36-45Механизм. Граница цикла
i < from.size() && i < bytes, но тело обращается кfrom[2*i]иfrom[2*i+1]. На последней итерацииi = min(from.size(), bytes)-1индекс2*i+1может достигать2*from.size()-1— далеко за концом строки. Проверкаif (x<0||y<0) break;стоит ПОСЛЕ OOB-чтения.Триггер. Любой путь write-meta/write-journal с укороченным hex-полем ("bitmap", "data", "block_csums", "ext_bitmap") в JSON-дампе (
disk_tool_journal.cpp:534-582,disk_tool_meta.cpp:570-853).Последствия. Heap OOB-чтение по JSON-вводу при restore метаданных/журнала → крах (ASan/hardened-аллокатор) или тихое чтение соседних байт → запись неверных байт в реконструированные метаданные.
Исправление. Условие
2*i+1 < from.size()(иi < bytes).Отклонённые false-positive
FP-1 —
deleteдля буфера, выделенного черезmalloc_or_die(defrag)src/nfs/nfs_kv_defrag.cpp:44-51Заявка. Буфер, выделенный
malloc_or_die(т.е.malloc), освобождается черезdelete→ потенциальная порча кучи.Почему FP. В кодовой базе нет перегруженного глобального
operator new/operator deleteнигде вsrc/. Стандартныйoperator deleteфорвардит вfree(), аmalloc_or_dieиспользует обычныйmallocбез спец-выравнивания. Заявленная порча кучи требует инструментированного/кастомного аллокатора, который не линкуется. Drop-in аллокаторы (tcmalloc/jemalloc) заменяютmallocиoperator new/deleteсогласованно, поэтому тоже не ломают. Это латентный hygiene/portability-нит (стоило бы использоватьfree(block_buf)), но не продакшен-баг. Наблюдаемый сбой ограничен ASan-сборками.FP-2 —
resize_initсчитаетnew_clean_entry_bitmap_sizeбез округленияsrc/disk_tool/disk_tool_resize.cpp:219Заявка.
new_clean_entry_bitmap_sizeвычисляется без округления вверх и расходится с системным размером bitmap.Почему FP. Триггер недостижим при валидных конфигурациях.
data_block_sizeобязан быть степенью двойки (blockstore_disk.cpp:125), кратнойbitmap_granularity(строка 173, значит тоже степень двойки), и отношение ≥8 (строка 177). Следовательноdata_block_size/bitmap_granularity— всегда степень двойки ≥8, т.е. всегда кратно 8 → floor и ceiling дают одинаковый результат, иnew_clean_entry_bitmap_size == dsk.clean_entry_bitmap_sizeдля всех достижимых конфигураций. Примеры искателя (16KiB/4KiB → отношение 4, отклоняется строкой 177; 48KiB → не степень двойки, отклоняется строкой 125) невалидны и отбраковываются на инициализации OSD. Floor-форма намеренно используется в эквивалентных вычислениях (osd.cpp:178,disk_simple_offsets.cpp:130). Косметическая несогласованность, не баг.Рекомендованный порядок устранения
len_pad4иxdr_array64-битной/overflow-safe арифметикой централизованно.Memory/logic vulnerabilities review using Opus 4.6 w/ ultracodeto Memory/logic vulnerabilities review using Opus 4.8 w/ ultracodeВот это прокомментирую, второй раз уже, короче там всё корректно только параметр называется is_free а должен называться is_used, это смущает всех ИИшек) - переименовал