Memory safety audit using AI (Claude Opus 4.8 + ultracode) #73

Open
opened 2026-05-29 05:04:28 +03:00 by maxpain · 0 comments

Аудит 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-границы — реальны).
Несколько находок я проверил лично по исходникам (отмечено ✔), включая две, найденные мной независимо.

⚠️ Многие баги — это OOB на недоверенном вводе (RPC от NFS-клиента, ответы/запросы пиров-OSD,
данные с диска). Для публичного облака это означает удалённый DoS / порчу памяти / утечку памяти.

Серьёзность: ? эксплуатируемая порча памяти/данных · ? краш/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_remaining 64-битным или проверять 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 чтение/запись при загрузке/recheck

src/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_compact memcpy).
Триггер: повреждённый/крафтнутый образ метаданных (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_start

journal.len. Фикс: проверять journal_start < journal.len и выравнивание.
(Связанный B3': init.cpp:773-778 — заголовок записи разыменовывается до проверки длины; over-read буфера чтения, если journal_block_size не делит 4 МБ.)


C. Heap-движок / EC — порча и OOB (нашёл независимо, подтверждено)

? C1. multilist_alloc_t ctor кладёт начальный блок не в тот бакет → двойное выделение 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_depth SQE до единственного
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) abort

src/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 (ниже приоритет)

  • ? E1 (UAF при остановке). ~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.
  • ? E2 (UAF, пограничная, 2/3). continue_ops (cluster_client.cpp:339-369) держит сырой next_op,
    который может быть удалён каскадом inc_wait/erase_op при обработке текущей op; гард перезапуска
    continuing_ops==2 сейчас — мёртвый код (удалён в коммите d7491595). Один скептик счёл точный триггер
    пока недостижимым — рекомендуется defensive-фикс (перечитывать op->next после обработки / вернуть гард).
  • ? E3 (утечки памяти). (a) EC partial-failure теряет массив subops при rollback (osd_primary_subops.cpp:769);
    (b) mark_partial_write игнорирует возвращаемый refcount (osd_primary_write.cpp:266); (c) zerocopy
    zc_free_list не потребляет NULL-маркер при батче без FREE-ops (msgr_send.cpp:300-341). Не порча, но рост памяти.

Проверено и ОТКЛОНЕНО как ложноположительные (5)

Adversarial-проверка отсеяла: (1) inc_wait reentrant 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_recheck sort-comparator NULL-deref (heap.cpp:1043-1048); ringloop SQE128
mis-index (ringloop.cpp:184-193, режим SQE128 не используется).


Резюме (приоритет для публичного облака)

# Где Класс Достижимость
A1 nfs xdr len_pad4 OOB-чтение ~4ГБ удалённый NFS-клиент
A2 exec_sec_read_bmp heap overflow запрос пира-OSD
A3 SEC_LIST read_remaining OOB-чтение ответ пира-OSD
A4 SEC_WRITE attr_len OOB→на диск запрос пира-OSD
A5 peering cur_set[role] OOB-чтение пир / порча метаданных
B1 heap entry size OOB чт/зап порча метаданных
B2 kv read_string OOB-чтение ~4ГБ порча KV-блока
B3 v1 journal_start OOB-запись порча журнала
C1 multilist ctor двойное выделение → порча журнал < блока
C2 ec_check_combination heap overflow скраб деградир. EC
C3 scrub stripes[role] OOB-чтение скраб деградир. EC
D1-D5 ublk / nfs-write / peering / heap краш/DoS разное
E1-E3 messenger / client / leaks UAF/утечки shutdown / edge

Самое срочное для публичного IaaS: A1 (если NFS-прокси доступен тенантам) и C2/C3/C1 (нормальная фоновая
активность кластера). Группа A2-A5/B* — укрепление против скомпрометированного пира и порчи данных на диске.

# Аудит 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-границы — реальны). Несколько находок я проверил лично по исходникам (отмечено ✔), включая две, найденные мной независимо. > ⚠️ Многие баги — это **OOB на недоверенном вводе** (RPC от NFS-клиента, ответы/запросы пиров-OSD, > данные с диска). Для публичного облака это означает удалённый DoS / порчу памяти / утечку памяти. Серьёзность: ? эксплуатируемая порча памяти/данных · ? краш/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_remaining` 64-битным или проверять `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 чтение/запись при загрузке/recheck **`src/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_compact` memcpy). **Триггер:** повреждённый/крафтнутый образ метаданных (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_start` > `journal.len`. **Фикс:** проверять `journal_start < journal.len` и выравнивание. *(Связанный B3': `init.cpp:773-778` — заголовок записи разыменовывается до проверки длины; over-read буфера чтения, если `journal_block_size` не делит 4 МБ.)* --- ## C. Heap-движок / EC — порча и OOB (нашёл независимо, подтверждено) ### ? C1. `multilist_alloc_t` ctor кладёт начальный блок не в тот бакет → двойное выделение 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_depth` SQE до единственного `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)` abort **`src/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 (ниже приоритет) - **? E1 (UAF при остановке).** `~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. - **? E2 (UAF, пограничная, 2/3).** `continue_ops` (`cluster_client.cpp:339-369`) держит сырой `next_op`, который может быть удалён каскадом `inc_wait`/`erase_op` при обработке текущей op; гард перезапуска `continuing_ops==2` сейчас — мёртвый код (удалён в коммите `d7491595`). Один скептик счёл точный триггер пока недостижимым — рекомендуется defensive-фикс (перечитывать `op->next` после обработки / вернуть гард). - **? E3 (утечки памяти).** (a) EC partial-failure теряет массив subops при rollback (`osd_primary_subops.cpp:769`); (b) `mark_partial_write` игнорирует возвращаемый refcount (`osd_primary_write.cpp:266`); (c) zerocopy `zc_free_list` не потребляет NULL-маркер при батче без FREE-ops (`msgr_send.cpp:300-341`). Не порча, но рост памяти. --- ## Проверено и ОТКЛОНЕНО как ложноположительные (5) Adversarial-проверка отсеяла: (1) `inc_wait` reentrant 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_recheck` sort-comparator NULL-deref (`heap.cpp:1043-1048`); `ringloop` SQE128 mis-index (`ringloop.cpp:184-193`, режим SQE128 не используется). --- ### Резюме (приоритет для публичного облака) | # | Где | Класс | Достижимость | |---|-----|-------|--------------| | A1 | nfs xdr `len_pad4` | OOB-чтение ~4ГБ | **удалённый NFS-клиент** | | A2 | `exec_sec_read_bmp` | heap overflow | запрос пира-OSD | | A3 | SEC_LIST `read_remaining` | OOB-чтение | ответ пира-OSD | | A4 | SEC_WRITE `attr_len` | OOB→на диск | запрос пира-OSD | | A5 | peering `cur_set[role]` | OOB-чтение | пир / порча метаданных | | B1 | heap entry `size` | OOB чт/зап | порча метаданных | | B2 | kv `read_string` | OOB-чтение ~4ГБ | порча KV-блока | | B3 | v1 `journal_start` | OOB-запись | порча журнала | | C1 | `multilist` ctor | двойное выделение → **порча** | журнал < блока | | C2 | ec_check_combination | **heap overflow** | скраб деградир. EC | | C3 | scrub `stripes[role]` | OOB-чтение | скраб деградир. EC | | D1-D5 | ublk / nfs-write / peering / heap | краш/DoS | разное | | E1-E3 | messenger / client / leaks | UAF/утечки | shutdown / edge | **Самое срочное для публичного IaaS:** A1 (если NFS-прокси доступен тенантам) и C2/C3/C1 (нормальная фоновая активность кластера). Группа A2-A5/B* — укрепление против скомпрометированного пира и порчи данных на диске.
maxpain changed title from Memory safety review using AI (Claude 4.8 + ultracode) to Memory safety review using AI (Claude Opus 4.8 + ultracode) 2026-06-09 20:20:19 +03:00
maxpain changed title from Memory safety review using AI (Claude Opus 4.8 + ultracode) to Memory safety audit using AI (Claude Opus 4.8 + ultracode) 2026-06-09 20:23:41 +03:00
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vitalif/vitastor#73
No description provided.