Files
faiss/tests
Trang Nguyen 1e5780fbbf Fix OOB read in Panorama::reconstruct via IndexPreTransform (T287094917) (#5562)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5562

Lionhead harness `faiss_rmdt_post_read_reconstruct_fuzzer` found a
heap out-of-bounds read at `Panorama.cpp:171`, reported as
`READ of size 5122903332` in `__asan_memcpy`.

Decoding the 232-byte crash input, it is `IxPT -> IxPT -> IxPT -> IxFp`
where every `IndexPreTransform` carries an **empty** transform chain and
the innermost `IndexFlatIPPanorama` has `d = 0x4C565349 = 1280725833`
(so `code_size = d * 4 = 5122903332`, exactly the ASAN read size),
`ntotal = 0`, and an empty `codes` vector. Three independent defects
combine:

1. `IndexFlatPanorama::reconstruct` never bounds-checks `key`. It
overrides `IndexFlat::reconstruct`, which does have
`FAISS_THROW_IF_NOT(key < ntotal)`, and drops the guard. Overriding
`reconstruct_n` likewise removes `IndexFlatCodes::reconstruct_n`'s
`i0 + ni <= ntotal` check. With `ntotal == 0` and empty `codes`,
`reconstruct(0, ...)` memcpy's `code_size` bytes out of a zero-length
buffer. `Panorama::reconstruct` takes an unsized `const uint8_t*`, so it
cannot self-validate -- the caller has to.

2. The `IxFP`/`IxFp` read branch does not range-check `d`. It is the one
index type that does not go through `read_index_header`, so it misses
that function's `FAISS_CHECK_RANGE(d, 0, (1 << 20) + 1)` and
`ntotal >= 0` checks. The `codes.size()` equality check below it does
not catch this, because `ntotal == 0` makes `num_slots == 0` and an
empty `codes` is then "valid".

3. `IndexPreTransform` deserialization skips all consistency checks when
the chain is empty -- the `chain[0].d_in == ixpt->d` /
`chain[nt-1].d_out == index->d` validation sits entirely inside
`if (nt > 0)`. With `nt == 0`, `IndexPreTransform::reconstruct` passes
the caller's buffer (sized from the outer `d`) straight to the
sub-index, so a mismatched sub-index `d` is also a write-side overflow,
for any sub-index type.

Each is fixed:

- `IndexFlatPanorama::reconstruct` / `reconstruct_n` now range-check
  against `ntotal`.
- The `IxFP`/`IxFp` branch applies the same `d` and `ntotal` checks as
  `read_index_header`.
- `read_index` requires `index->d == ixpt->d` when the `IxPT` chain is
  empty.

Two adjacent gaps in the same audit are closed as well:

- `IHfP` (`IndexHNSWFlatPanorama`) never validated `cum_sums.size()`,
  unlike the equivalent `IxFP` check. Both `get_cum_sum()` and
  `HNSW.cpp`'s Panorama search path index it at stride
  `pano.n_levels + 1`, so a short vector is an OOB read at search time.
  It also had no explicit `n_levels > 0` guard.
- `ArrayInvertedListsPanorama::get_single_code` bounds-checked with
  `assert`, which is compiled out in opt builds. It reaches the same
  `Panorama::reconstruct` sink via
  `IndexIVFFlatPanorama::reconstruct_from_offset`.

Reviewed By: mnorris11

Differential Revision: D118495504

fbshipit-source-id: 46bc7a1a6c9335a5589e20221b9478ae5505b9da
2026-09-08 13:56:04 -07:00
..
2026-05-05 16:54:09 -07:00
2026-05-01 09:55:55 -07:00