Memory safety audit using AI (Claude Opus 4.8 + ultracode) #73
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 3.0.12 — memory-safety / UB / низкоуровневая корректность
Версия: 3.0.12, коммит
b4aab7a7. Дополнение к аудиту повреждения данных(
AUDIT_data_corruption_3.0.12.md) — отдельный проход с фокусом наUAF / OOB / double-free / uninit / nullptr-deref / переполнения / UB / некорректные касты / STL-misuse.
Методология. Файлы читались целиком; каждая находка независимо проверялась 3 скептиками
(границы/guard · lifetime/ownership · реально-ли-UB-и-используется), по умолчанию пытавшимися её
опровергнуть. Из 37 кандидатов 5 отклонены как ложноположительные (см. конец), 2 — пограничные.
Учтено:
*_or_die()аварийно завершает процесс (проверка на NULL после них не нужна), packed-структурычерез
reinterpret_cast— by-design, гонок в однопоточном event-loop нет (но UAF через async-границы — реальны).Несколько находок я проверил лично по исходникам (отмечено ✔), включая две, найденные мной независимо.
Серьёзность: ? эксплуатируемая порча памяти/данных · ? краш/DoS · ? утечка/частный случай.
A. OOB на недоверенном сетевом вводе (remote / peer)
? A1. NFS XDR: переполнение в
len_pad4()→ ~4 ГБ OOB-чтение у удалённого клиента ✔src/nfs/proto/xdr_impl_inline.h:94-97, 132-148(len_pad4/xdr_bytes).len_pad4(len) = ((len+3)/4)*4считается в 32-битной арифметике; приlen=0xFFFFFFFF(len+3)оборачивается →
padded=0. Проверкаif (avail < 4+padded)проходит приavail>=4, после чегоdata->size = len (0xFFFFFFFF),data->data = buf+4. Любая строка из NFS-запроса (имя файла вLOOKUP/CREATE/REMOVE/RENAME, путь, fh) проходит здесь; потребитель (
std::string(data, 0xFFFFFFFF),напр.
nfs_kv_lookup.cpp:17) копирует ~4 ГБ из крошечного буфера → крах / утечка памяти.Триггер: один RPC (например LOOKUP3) с длиной имени
0xFFFFFFFF. Удалённо, без аутентификации (если NFS-прокси доступен тенанту).Фикс: проверять
lenпротивavail/maxlenДОlen_pad4; считать padding в 64 бит и отвергатьlen > avail.(Связанный минорный: RDMA-ветка
xdr_bytesстроки 132-141 ставитdata->size=lenбез проверки против размера RDMA-чанка — только сборки WITH_RDMACM.)? A2.
exec_sec_read_bmp: 32-битное переполнение размера ответа → heap overflow ✔src/osd/osd_secondary.cpp:254, 270.malloc_or_die(n * (8 + clean_entry_bitmap_size))—clean_entry_bitmap_sizeэтоuint32_t, поэтому произведение вычисляется в 32-битной беззнаковойарифметике и оборачивается, а цикл (
for i<n) пишет полный объём 64-битной указательной арифметикой(
cur_buf += 8+clean_entry_bitmap_size) → запись далеко за буфер.Триггер: пир-OSD шлёт
OSD_OP_SEC_READ_BMPсlen ≈ 26 МБпри крупномdata_block_size(
clean_entry_bitmap_size=4096):n≈1.09M,n*4104≈4.47e9оборачивается до ~178 МБ malloc, цикл пишет ~4.47 ГБ.Фикс: считать размер в
size_t/uint64_tи ограничитьn.? A3. SEC_LIST: 32-битное усечение
read_remaining→ недоразмеренный буфер, OOB-чтениеsrc/client/msgr_receive.cpp:402-404(потребительosd_peering_pg.cpp:419-436).cl->read_remaining— знаковыйint(messenger.h:79); для ответа SEC_LIST он =sizeof(obj_ver_id)*retval(
retval— int64 от пира, проверен только>0).24*retvalусекается до 32 бит → буферop->bufвыделяется усечённого размера, а потребитель итерирует полным 64-битным
retval→ массовое OOB-чтениеresize. Триггер: пир в ответе SEC_LIST ставитretval=0x0AAAAAAB→24*retvalусекается до 8 →8-байтный буфер, обход ~179M элементов. Фикс: сделать
read_remaining64-битным или проверятьretvalпротив разумного предела. (Это же — находка messenger-core про усечение wire-длин в тот же
int.)? A4. SEC_WRITE:
attr_lenс провода не проверяется противclean_entry_bitmap_size→ OOB-чтение, записываемое на дискsrc/client/osd_ops.h:96→msgr_receive.cpp:275-288→osd_secondary.cpp:160-168→blockstore_heap.cpp:1573,1600/v1/write.cpp:175.Приёмник выделяет буфер битмапа ровно из
attr_lenбайт (или 4-байтное inline-поле приattr_len<=4),а blockstore безусловно делает
memcpy(dst, op->bitmap, clean_entry_bitmap_size). Приattr_len < clean_entry_bitmap_size(типично приblock_size≥1M) читается за буфер, и лишние байты кучипопадают в записанный на диск битмап (утечка + порча битмапа). Триггер: пир шлёт SEC_WRITE с
attr_len=8приclean_entry_bitmap_size=32. Фикс: отвергатьattr_len != clean_entry_bitmap_size.? A5. Peering: OOB-чтение
pg.cur_setпо непроверенной роли из устаревших записей листингаsrc/osd/osd_peering_pg.cpp:289-316(доступ на 302). Для версий старше выбранной целевойfinish_object()читаетpg->cur_set[list[i].oid.stripe & STRIPE_MASK].STRIPE_MASK=4095, а проверкаreplica >= pg_sizeесть только в веткеif (!target_ver)(строки 119-134); т.к. список отсортирован поверсии убыванием, после установки
target_verвсе старые записи пропускают валидацию.cur_setимеетровно
pg_size (≤256)элементов → чтение до ~4095*8 байт за вектор, и прочитанное значение влияет насостояние PG. Триггер: объект с корректной новой версией и старой копией, чья роль кодирует значение
≥ pg_size (порча метаданных пира или баг/злонамеренный пир в ответе SEC_LIST). Фикс: валидировать роль для всех записей.
B. OOB на недоверенных данных с диска
? B1. Heap: размер записи
sizeне проверяется против раскладки типа → OOB чтение/запись при загрузке/rechecksrc/blockstore/blockstore_heap.cpp—read_blocks(292-438), аксессоры (159-216), сайты 884-945 / 1080-1094 / 1641-1685 / 1725-1740.read_blocksпроверяетsizeлишь как≤ block,≥ sizeof(heap_entry_t)=40и (для small/intent)≥56,но никогда не проверяет, что
sizeвмещает раскладку конкретного типа: нет минимума для BIG_WRITE (44)и BIG_INTENT (52), и нигде нет проверки
size ≥ get_size()(которая включает 1-2 битмапа + массив чексумм).CRC считается ровно по
sizeбайтам, поэтому усечённая запись с пересчитанным CRC проходит все проверки.load_blocksвыделяетmalloc(size + ...)и копируетsizeбайт, а аксессоры (get_int_bitmap/get_checksums/big()) считают фиксированные смещения независимо отsize→ читают/пишут за пределамималенькой аллокации (в т.ч. при recheck-проверке чексумм
calc_block_checksumsи вadd_big_intent/punch_holes/add_compactmemcpy).Триггер: повреждённый/крафтнутый образ метаданных (BIG_INTENT с
size=40и валидным CRC). Фикс:после проверки CRC валидировать
size ≥ wr->get_size(this)для каждого типа.(Эту брешь независимо нашли 5 finder'ов: heap-entry-ptrarith, heap-csum-buffers, heap-init-parse ×2, heap-reshard — все 3/3.)
? B2. KV: переполнение в проверке границ
read_string→ ~4 ГБ OOB-чтение из повреждённого блока ✔src/kv/kv_db.cpp:236-241(черезkv_block_t::parse).*pos(int) +len(uint32_t) >size(int)вычисляется в 32-битной беззнаковой арифметике; при
len=0xFFFFFFF0сумма оборачивается до малогозначения → проверка проходит →
std::string(data+*pos, len)читает ~4 ГБ из буфера вkv_block_size(4096) байт.Триггер: чтение/rescue KV-индекса с повреждённым полем длины (key/value/key_ge/...). Фикс:
проверять
(uint64_t)*pos + len > (uint64_t)size.? B3. V1:
journal_startиз суперблока журнала не проверяется → OOB-запись в буфер журналаsrc/blockstore/v1/init.cpp:622, 636-651.journal_pos = je_start->journal_startберётся прямо с диска;валидируются только magic/type/crc/size, но не диапазон значения. Затем (при дефолтном
inmemory_journal=true)submitted_buf = journal.buffer + journal_posиio_uring_prep_readv(..., journal.offset + journal_pos)→запись до 4 МБ за конец
journal.buffer. Триггер: журнал с валидным CRC суперблока, ноjournal_startC. Heap-движок / EC — порча и OOB (нашёл независимо, подтверждено)
? C1.
multilist_alloc_tctor кладёт начальный блок не в тот бакет → двойное выделение buffer-area → порча данных ✔src/blockstore/multilist.cpp:23. Конвенция аллокатора: блок размераsлежит вheads[(s<maxn?s:maxn)-1](см.
do_free:272,use_full:224). Конструктор же безусловно делаетheads[maxn-1]=1для блока размераcount.При
count < maxnэто неверный бакет → устаревшая голова → повторная выдача того же региона → двойноевыделение buffer-area (порча данных) и затем abort.
count = journal_len/granularity,maxn = data_block_size/granularity - 1(heap.cpp:254); кросс-проверки нет. Триггер: журнал меньше одногоблока данных (напр. журнал 1 МБ,
block_size=4M). Фикс:heads[(count<maxn?count:maxn)-1]=1.? C2. EC-скраб: heap overflow в
ec_check_combination, когдаstripe_count < pg_size✔ (нашёл независимо)src/osd/osd_rmw.cpp:1211-1217(alloc 1341); вызов изosd_scrub.cpp:544.Для деградированного EC-объекта
submit_scrub_subops(osd_scrub.cpp:397-404) ставитstripe_count= числу не-outdated чанков, что может быть < pg_size.tmp_bufвыделяется какstripe_count*(chunk_size+bitmap_size), но индексируется по ролиtmp_buf + i*chunk_size,i∈[0,pg_size).При
pg_size > stripe_count(типично: один outdated чанк в пуле 4+2 →stripe_count=5,pg_size=6)запись реконструкции/паритета уходит на
tmp_buf+5*chunk_sizeза пределы буфера → OOB-запись кучи до ~chunk_size(напр. 128 КБ) при штатном скрабе деградированного EC-объекта (для EC с ≥2 parity). Брутфорс при этом
гарантированно вызывается (
live_roles=5 > pg_minsize=4). Фикс: выделятьtmp_bufисходя изpg_size, неstripe_count.? C3. Скраб:
stripes[]индексируется по EC-роли вместо индекса массива → OOB-чтение ✔ (= логика-#9)src/osd/osd_scrub.cpp:630—op_data->stripes[chunk.role].missingдолжно бытьstripes[i].Для деградированного объекта
stripes[]упакован доstripe_count < pg_size, аchunk.roleдоходит доpg_size-1→ чтение за границей массива. Фикс:op_data->stripes[i].missing.D. Краши / DoS
? D1. ublk: непроверенный
get_sqe()→ NULL-разыменование приqueue_depthбольше размера кольцаsrc/client/ublk_server.cpp:739-752.submit_request()разыменовываетringloop->get_sqe()без проверкина NULL; кольцо фиксированного размера 1024, а
start_device()берётqueue_depthSQE до единственногоsubmit().queue_depth(uint16, до 4096 в ядре) без верхней границы. Триггер:--queue_depth 2048→SIGSEGV на 1024-й итерации. Фикс: проверять
get_sqe()на NULL / ограничиватьqueue_depth.? D2. NFS shared redirect-write: OOB-чтение старого буфера при разрежённой записи за EOF (утечка на диск) ✔-подтв. 3/3
src/nfs/nfs_kv_write.cpp:420(буфер выделен 284). Префикс пишется какop->iov.push_back(st->aligned_buf, st->offset), гдеst->offset— необрезанное смещение из NFS-запроса,а
aligned_buf— лишьold_sizeбайт. При записи за EOF (offset > old_size) читаетсяoffset-old_sizeбайт за буфером, и они уходят на диск как префикс файла (утечка памяти клиенту). Фикс: ограничивать
длину префикса
old_size, остальное — нулями.? D3. NFS shared redirect-write: неверная длина zero-padding (
!has_old) →assert(left==0)abortsrc/nfs/nfs_kv_write.cpp:438—add_zero(op, st->offset)вместоst->new_size-(st->offset+st->size);сумма iov ≠
op->len→ abort вadd_iov(cluster_client.cpp:1232). DoS прокси. Фикс: использовать правильную длину (как в веткеhas_old, строка 435).? D4. Peering: signed-shift UB и усечение 64-битной маски для EC-ролей ≥32 / ≥64
src/osd/osd_peering_pg.cpp:162,164—1 << replica(int) приreplicaдо 255 (UB при ≥31/≥32), аhas_roles(64 бит) не вмещает роли ≥64 →n_rolesнедосчитывается → объект может быть неверно признаннеполным. Только EC с pg_size>32 (необычно). Фикс:
1ULL << replicaи счётчик ролей без 64-битной маски.? D5. Heap: возможное NULL-разыменование при обходе ROLLBACK-цепочки
src/blockstore/blockstore_heap.cpp:1764-1770(add_commit/add_rollback) —wr = prev(wr)затемwhile (wr->version > ...)без проверкиwrна NULL для крафтнутой/повреждённой цепочки. Крах. Фикс: проверятьwrв цикле.E. UAF / утечки / shutdown (ниже приоритет)
~osd_messenger_t(messenger.cpp:166-170) удаляет клиентов(
force_delete=true, освобождаетin_buf/send_list) доdestroy_iothreads(); приiothread_count>0ядро может завершить in-flight
recvmsg/sendmsgв уже освобождённый буфер → UAF. Фикс: сперва дренировать/джойнить iothreads.continue_ops(cluster_client.cpp:339-369) держит сыройnext_op,который может быть удалён каскадом
inc_wait/erase_opпри обработке текущей op; гард перезапускаcontinuing_ops==2сейчас — мёртвый код (удалён в коммитеd7491595). Один скептик счёл точный триггерпока недостижимым — рекомендуется defensive-фикс (перечитывать
op->nextпосле обработки / вернуть гард).osd_primary_subops.cpp:769);(b)
mark_partial_writeигнорирует возвращаемый refcount (osd_primary_write.cpp:266); (c) zerocopyzc_free_listне потребляет NULL-маркер при батче без FREE-ops (msgr_send.cpp:300-341). Не порча, но рост памяти.Проверено и ОТКЛОНЕНО как ложноположительные (5)
Adversarial-проверка отсеяла: (1)
inc_waitreentrant double-continue UAF (cluster_client.cpp:237-279) —гард есть; (2)
msgr_rdmaусечениеsend_out_size— ограничено валидацией; (3) writeback группировкаDELETE+WRITE в
repeat_ops_for— латентно/недостижимо; (4)v1/write.cpp:31-36декремент итератора map заbegin() — на деле не происходит; (5)
osd_primary.cppсравнение «висячего»object_state— без разыменования, безвредно.Пограничные на ревью:
finish_rechecksort-comparator NULL-deref (heap.cpp:1043-1048);ringloopSQE128mis-index (
ringloop.cpp:184-193, режим SQE128 не используется).Резюме (приоритет для публичного облака)
len_pad4exec_sec_read_bmpread_remainingattr_lencur_set[role]sizeread_stringjournal_startmultilistctorstripes[role]Самое срочное для публичного IaaS: A1 (если NFS-прокси доступен тенантам) и C2/C3/C1 (нормальная фоновая
активность кластера). Группа A2-A5/B* — укрепление против скомпрометированного пира и порчи данных на диске.
Memory safety review using AI (Claude 4.8 + ultracode)to Memory safety review using AI (Claude Opus 4.8 + ultracode)Memory safety review using AI (Claude Opus 4.8 + ultracode)to Memory safety audit using AI (Claude Opus 4.8 + ultracode)