Skip to content

refactor(index): migrate FlatBuffers from flatc IDL to kotatsu reflection - #486

Closed
16bit-ykiko wants to merge 4 commits into
mainfrom
migrate/flatbuffers-to-kotatsu-reflection-v2
Closed

16bit-ykiko wants to merge 4 commits into
mainfrom
migrate/flatbuffers-to-kotatsu-reflection-v2

Conversation

@16bit-ykiko

@16bit-ykiko 16bit-ykiko commented Jul 5, 2026 •

Copy link
Copy Markdown
Member

Summary

Replace the flatc-generated serialization of TUIndex / ProjectIndex / MergedIndex with kotatsu's reflection-driven FlatBuffers codec. The in-memory types are serialized directly — no IDL, no generated code, no hand-written builder/reader walks. Supersedes #430, redone on top of current main (multi-level symbol table, dep-hash staleness) and the reworked kotatsu visitor codec.

  • Wire layout derives from the reflected structs themselves. Runtime-only state is marked kota::meta::skip<> (TUIndex::file_indices, IncludeGraph::file_table, Impl::canonical_ref_counts / occurrences_cache); derived state is rebuilt after decoding.
  • Type-level codec adapters in index/serialization.h bridge the non-reflectable leaves: RelationKind (u32), SymbolKind (u8), roaring bitmaps (bytes, same non-portable format as before), std::chrono::milliseconds (i64), and the shard PathPool (string table, re-interned on decode). They apply anywhere the type appears — fields, map keys/values, elements.
  • Zero-copy lazy paths kept. Occurrence / Relation / LocalSourceRange gain defaulted operator<=>; the codec writes map entries sorted by key, and MergedIndex's buffer-only lookups (lookup / need_update / has_contribution / find_symbol / content / line_starts) binary-search that order through kotatsu's table_view proxy instead of flatc accessors. find_symbol upgrades from a linear scan to binary search for free.
  • Compaction is explicit now: removed-masked state is vacuumed in place by Impl::compact() before encoding (previously interleaved with the hand-written builder walk), so the written bytes and the runtime object stay equivalent; serialize() takes a mutable self.
  • TUIndex::build() drains the FileID-keyed scratch map into the canonical path-id-keyed path_file_indices instead of copying (the refactor(index): migrate FlatBuffers from flatc IDL to kotatsu reflection #430 approach doubled peak index memory in the worker). Tests access per-file indexes via path ids.
  • Verified decoding: kotatsu's schema-driven deep verifier runs before every decode and on MergedIndex::load, so corrupt or truncated blobs are rejected instead of read out of bounds. TUIndex::from takes an explicit size for the same reason.
  • index_format_version bumps to 2. flatc-era blobs are additionally rejected by the kotatsu buffer identifier, so stale caches rebuild in the background as designed.

Drops

  • src/index/schema.fbs and the flatc codegen target
  • the direct flatbuffers FetchContent + conda deps (now transitive via kota::codec::flatbuffers)
  • ~600 lines of hand-written builder/reader code across the three index types

Net -317 LoC.

Dependencies

kotatsu is pinned to b4620e9 (clice-io/kotatsu#178, the codec fixes this migration relies on: typed map-key ordering, llvm container entry protocols, deep verification, inline-struct criterion). Repoint to main once that PR merges.

Test plan

  • RelWithDebInfo build clean (clice-core + unit_tests), local kotatsu source override
  • unit_tests — 812 passed / 12 skipped, including MergedIndex round-trips, lazy lookups on serialized shards, stale-version and garbage shard rejection, ProjectIndex path remapping across sessions
  • kotatsu side: 1379 passed incl. new map-protocol and truncation/bit-flip robustness suites under ASan/UBSan

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Improved index persistence with a bumped on-disk format version and codec-based serialization.
    • Enhanced cross-file/header indexing by resolving include locations via the first-include lookup.
  • Bug Fixes
    • Fixed TU index decoding to honor the provided blob size.
    • Strengthened shard/index validation by verifying FlatBuffer structure and rejecting mismatched format versions.
    • Improved merged-index compaction so removed-state changes are reflected consistently after serialization.
  • Chores
    • Updated FlatBuffers integration to use the unified codec dependency path.

…tion

Replace the flatc-generated serialization of TUIndex / ProjectIndex /
MergedIndex with kotatsu's reflection-driven FlatBuffers codec. The in-memory
types are now serialized directly — no IDL, no generated code, no hand-written
builder/reader walks.

- Wire layout is derived from the reflected structs themselves. Runtime-only
  state is marked kota::meta::skip<> (TUIndex::file_indices,
  IncludeGraph::file_table, Impl::canonical_ref_counts / occurrences_cache)
  and derived state is rebuilt after decoding.
- Non-reflectable leaves are bridged by type-level codec adapters in
  index/serialization.h: RelationKind (u32), SymbolKind (u8), roaring
  bitmaps (bytes, non-portable format as before), std::chrono::milliseconds
  (i64) and the shard PathPool (string table, re-interned on decode).
- Occurrence / Relation / LocalSourceRange gain defaulted operator<=>; the
  codec writes map entries sorted by key, which the zero-copy buffer paths
  binary-search. MergedIndex's lazy lookups (lookup / need_update /
  has_contribution / find_symbol / content / line_starts) now run on
  kotatsu's table_view proxy instead of flatc accessors.
- Compaction of removed-masked state moves from serialize-time copying into
  an explicit Impl::compact() that vacuums in place; serialize() therefore
  takes a mutable self.
- TUIndex::build() drains the FileID-keyed scratch map into the canonical
  path-id-keyed path_file_indices instead of copying, halving peak index
  memory in the worker; tests access per-file indexes via path ids now.
- Decoding runs kotatsu's schema-driven deep verifier, so corrupt or
  truncated blobs are rejected instead of read out of bounds; TUIndex::from
  takes an explicit size for the same reason.
- index_format_version bumps to 2. flatc-era blobs are additionally rejected
  by the kotatsu buffer identifier, so stale caches rebuild in the
  background as designed.

Drops src/index/schema.fbs, the flatc codegen target and the direct
flatbuffers/conda dependencies (flatbuffers now comes transitively via
kota::codec::flatbuffers). Depends on the kotatsu branch
fix/fbs-reflection-codec, pinned in cmake/package.cmake.
@coderabbitai

coderabbitai Bot commented Jul 5, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR replaces the manual FlatBuffers schema/codegen workflow with kota::codec-based serialization for index data, updates index containers and versioning, switches TU index lookups to path-keyed storage, and adjusts call sites and tests to the new serialized layout.

Changes

FlatBuffers to kota::codec migration

Layer / File(s) Summary
Switch build to kotatsu-provided flatbuffers codec
CMakeLists.txt, cmake/package.cmake, pixi.toml
Removes the custom schema-generation target and standalone flatbuffers dependency, and switches the build to the kotatsu-provided codec path.
Define kota::codec adapters and comparison operators
src/index/serialization.h, src/semantic/relation_kind.h, src/syntax/token.h, src/index/tu_index.h, src/index/include_graph.h, src/index/shared.h, src/index/project_index.h
Adds codec visitors, comparison operators, index format versioning, and serialization-skip annotations used by the new persistence layer.
Rewrite MergedIndex serialization and query paths
src/index/merged_index.cpp, src/index/merged_index.h
Updates merged index persistence, format validation, compaction, and buffer-backed lookup paths to use table views and codec helpers.
Rewrite ProjectIndex serialization
src/index/project_index.cpp, src/index/project_index.h
Reworks project index persistence around a persisted root object with remapped paths, shards, and symbol references.
Refactor TUIndex build and serialization; migrate to path-keyed file indices
src/index/tu_index.cpp, src/index/tu_index.h
Moves TU index storage to path-keyed file indices and updates serialization and deserialization to the new codec flow.
Update TUIndex::from call sites
src/server/compiler/compiler.cpp, src/server/index/background_indexer.cpp
Passes explicit buffer sizes into TU index loading and uses the new include-location lookup helper.
Update tests for path_file_indices and codec fixtures
tests/unit/index/index_query_tests.cpp, tests/unit/index/merged_index_tests.cpp, tests/unit/index/tu_index_tests.cpp
Adjusts tests for path-keyed indices, the new include lookup helper, and codec-encoded shard fixtures.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant TUIndex
  participant kota_codec_fbs
  participant MergedIndex

  Caller->>TUIndex: from(data, size)
  TUIndex->>kota_codec_fbs: from_flatbuffer(span)
  kota_codec_fbs-->>TUIndex: decoded TUIndex or failure
  TUIndex-->>Caller: TUIndex

  Caller->>MergedIndex: load(path)
  MergedIndex->>kota_codec_fbs: verify_flatbuffer<Impl>(bytes)
  kota_codec_fbs-->>MergedIndex: verification result
  MergedIndex->>kota_codec_fbs: table_view<Impl>(bytes)
  kota_codec_fbs-->>MergedIndex: format_version and fields
  MergedIndex-->>Caller: loaded index or empty

  Caller->>MergedIndex: serialize(out)
  MergedIndex->>MergedIndex: compact()
  MergedIndex->>kota_codec_fbs: to_flatbuffer(*impl)
  kota_codec_fbs-->>MergedIndex: encoded bytes
  MergedIndex-->>Caller: write bytes
Loading

Possibly related PRs

  • clice-io/clice#279: Both PRs rework index persistence and FlatBuffers-backed load/serialize paths for the index types.
  • clice-io/clice#402: Both modify TU index build/serialization surfaces and must align on the TUIndex::from API and serialized blob handling.
  • clice-io/clice#485: Both modify src/index/merged_index.{h,cpp} around persisted layout, verification, and staleness-related behavior.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: migrating index FlatBuffers from flatc-generated IDL to kotatsu reflection.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch migrate/flatbuffers-to-kotatsu-reflection-v2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/index/project_index.cpp (1)

91-96: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Silent serialize failure persists an empty blob without any diagnostic.

On encode failure the assert is a no-op in release builds and the function returns having written nothing to os, so the caller persists an empty/absent blob. It recovers on next load (rejected → rebuild), but the failure is invisible in production. Consider logging a warning so the failed persist is diagnosable rather than silently swallowed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index/project_index.cpp` around lines 91 - 96, The ProjectIndex
serialization path silently drops failures when kota::codec::fbs::to_flatbuffer
returns no buffer, since the assert is disabled in release builds and the
function just returns from the persist flow. Update the ProjectIndex
write/persist logic around encoded handling to emit a warning or error when
serialization fails before returning, so the failure is visible in production
while keeping the existing early-exit behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/index/merged_index.cpp`:
- Around line 356-372: The rebuild of MergedIndex::canonical_ref_counts is
trusting the decoded max_canonical_id field, which can cause an excessive
allocation from a corrupted but structurally valid shard. In
MergedIndex::load_in_memory (or the nearby rebuild block), compute the needed
size from the actual canonical ids referenced in header_contexts and
compilation_contexts, then clamp or validate before calling resize; keep the
existing bounds-checked count_ref logic but avoid using index.max_canonical_id
as the direct allocation size. Use the surrounding load/verify flow in
MergedIndex to ensure the derived count array is sized only to what is
semantically required.

In `@src/index/serialization.h`:
- Around line 146-162: The PathPool deserialization in deserialize_visit<Vis,
clice::index::PathPool, Config>::visit is silently dropping empty strings, which
changes later path_id() assignments and can corrupt id-based references. Update
the loop so an empty entry is treated as a decode failure: detect it while
rebuilding out.paths/cache and return false instead of skipping it, keeping the
PathPool state consistent with the original serialized ordering.

---

Nitpick comments:
In `@src/index/project_index.cpp`:
- Around line 91-96: The ProjectIndex serialization path silently drops failures
when kota::codec::fbs::to_flatbuffer returns no buffer, since the assert is
disabled in release builds and the function just returns from the persist flow.
Update the ProjectIndex write/persist logic around encoded handling to emit a
warning or error when serialization fails before returning, so the failure is
visible in production while keeping the existing early-exit behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bf58cd1a-bae3-4efa-9f83-3fdcb297961c

📥 Commits

Reviewing files that changed from the base of the PR and between 403ffab and 6dd84b5.

⛔ Files ignored due to path filters (1)
  • pixi.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • CMakeLists.txt
  • cmake/package.cmake
  • pixi.toml
  • src/index/include_graph.h
  • src/index/merged_index.cpp
  • src/index/merged_index.h
  • src/index/project_index.cpp
  • src/index/schema.fbs
  • src/index/serialization.h
  • src/index/tu_index.cpp
  • src/index/tu_index.h
  • src/semantic/relation_kind.h
  • src/server/compiler/compiler.cpp
  • src/server/index/background_indexer.cpp
  • src/syntax/token.h
  • tests/unit/index/index_query_tests.cpp
  • tests/unit/index/merged_index_tests.cpp
  • tests/unit/index/tu_index_tests.cpp
💤 Files with no reviewable changes (2)
  • src/index/schema.fbs
  • pixi.toml

Comment on lines +356 to 372
// Rebuild the derived reference counts from the decoded contexts. Ids are
// bounds-checked: the verifier guarantees structure, not semantics, so a
// corrupted-but-well-formed shard must not index out of range.
index.canonical_ref_counts.resize(index.max_canonical_id, 0);

for(auto entry: *root->header_contexts()) {
HeaderContext context;
auto path = entry->path_id();
context.version = entry->version();
for(auto include: *entry->includes()) {
index.canonical_ref_counts[include->canonical_id()] += 1;
context.includes.emplace_back(*safe_cast<IncludeContext>(include));
}
index.header_contexts.try_emplace(path, std::move(context));
}

for(auto entry: *root->compilation_contexts()) {
CompilationContext context;
auto path = entry->path_id();
context.version = entry->version();
context.canonical_id = entry->canonical_id();
context.build_at = entry->build_at();
for(auto include: *entry->include_locations()) {
context.include_locations.emplace_back(*safe_cast<IncludeLocation>(include));
}
if(entry->dep_hashes()) {
for(auto dep: *entry->dep_hashes()) {
context.dep_hashes.emplace_back(*safe_cast<DepHash>(dep));
}
auto count_ref = [&](std::uint32_t canonical_id) {
if(canonical_id < index.canonical_ref_counts.size()) {
index.canonical_ref_counts[canonical_id] += 1;
}
index.compilation_contexts.try_emplace(path, std::move(context));
}

// Count ref counts from compilation contexts.
for(auto entry: *root->compilation_contexts()) {
index.canonical_ref_counts[entry->canonical_id()] += 1;
}

// Deserialize removed bitmap.
if(root->removed() && root->removed()->size() > 0) {
index.removed = read_bitmap(root->removed());
}

for(auto entry: *root->occurrences()) {
index.occurrences.try_emplace(*safe_cast<Occurrence>(entry->occurrence()),
read_bitmap(entry->context()));
}

for(auto entry: *root->relations()) {
auto& relations = index.relations[entry->symbol()];
for(auto relation_entry: *entry->relations()) {
relations.try_emplace(*safe_cast<Relation>(relation_entry->relation()),
read_bitmap(relation_entry->context()));
};
for(auto& [_, context]: index.header_contexts) {
for(auto& include: context.includes) {
count_ref(include.canonical_id);
}
}

if(root->content()) {
index.content = root->content()->str();
for(auto& [_, context]: index.compilation_contexts) {
count_ref(context.canonical_id);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unbounded canonical_ref_counts resize from an untrusted decoded field.

index.canonical_ref_counts.resize(index.max_canonical_id, 0) trusts max_canonical_id straight out of the decoded shard. verify_flatbuffer/from_flatbuffer only guarantee structural (offset/bounds) validity, not that this scalar is semantically reasonable. A corrupted or maliciously-crafted (but structurally valid) shard on disk with max_canonical_id near UINT32_MAX triggers a multi-GB allocation here, which is exactly the "must never crash the server" scenario the surrounding comments call out (MergedIndex::load's deep-verify docstring at Line 388-391). This path runs whenever a shard is mutated (merge/remove/merge_symbols → load_in_memory), not just on read.

Bound the resize by what's actually referenced in the decoded contexts instead of trusting the raw field directly.

🛡️ Proposed fix
-    index.canonical_ref_counts.resize(index.max_canonical_id, 0);
-    auto count_ref = [&](std::uint32_t canonical_id) {
-        if(canonical_id < index.canonical_ref_counts.size()) {
-            index.canonical_ref_counts[canonical_id] += 1;
-        }
-    };
-    for(auto& [_, context]: index.header_contexts) {
-        for(auto& include: context.includes) {
-            count_ref(include.canonical_id);
-        }
-    }
-    for(auto& [_, context]: index.compilation_contexts) {
-        count_ref(context.canonical_id);
-    }
+    // Bound the allocation by the maximum id actually referenced by the
+    // decoded contexts (clamped to max_canonical_id), rather than trusting
+    // max_canonical_id directly — a corrupted-but-structurally-valid shard
+    // must not be able to force an unbounded allocation here.
+    std::uint32_t max_ref = 0;
+    for(auto& [_, context]: index.header_contexts) {
+        for(auto& include: context.includes) {
+            max_ref = std::max(max_ref, include.canonical_id);
+        }
+    }
+    for(auto& [_, context]: index.compilation_contexts) {
+        max_ref = std::max(max_ref, context.canonical_id);
+    }
+    auto safe_size = std::min<std::uint64_t>(index.max_canonical_id,
+                                             static_cast<std::uint64_t>(max_ref) + 1);
+    index.canonical_ref_counts.resize(safe_size, 0);
+    auto count_ref = [&](std::uint32_t canonical_id) {
+        if(canonical_id < index.canonical_ref_counts.size()) {
+            index.canonical_ref_counts[canonical_id] += 1;
+        }
+    };
+    for(auto& [_, context]: index.header_contexts) {
+        for(auto& include: context.includes) {
+            count_ref(include.canonical_id);
+        }
+    }
+    for(auto& [_, context]: index.compilation_contexts) {
+        count_ref(context.canonical_id);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Rebuild the derived reference counts from the decoded contexts. Ids are
// bounds-checked: the verifier guarantees structure, not semantics, so a
// corrupted-but-well-formed shard must not index out of range.
index.canonical_ref_counts.resize(index.max_canonical_id, 0);
for(auto entry: *root->header_contexts()) {
HeaderContext context;
auto path = entry->path_id();
context.version = entry->version();
for(auto include: *entry->includes()) {
index.canonical_ref_counts[include->canonical_id()] += 1;
context.includes.emplace_back(*safe_cast<IncludeContext>(include));
}
index.header_contexts.try_emplace(path, std::move(context));
}
for(auto entry: *root->compilation_contexts()) {
CompilationContext context;
auto path = entry->path_id();
context.version = entry->version();
context.canonical_id = entry->canonical_id();
context.build_at = entry->build_at();
for(auto include: *entry->include_locations()) {
context.include_locations.emplace_back(*safe_cast<IncludeLocation>(include));
}
if(entry->dep_hashes()) {
for(auto dep: *entry->dep_hashes()) {
context.dep_hashes.emplace_back(*safe_cast<DepHash>(dep));
}
auto count_ref = [&](std::uint32_t canonical_id) {
if(canonical_id < index.canonical_ref_counts.size()) {
index.canonical_ref_counts[canonical_id] += 1;
}
index.compilation_contexts.try_emplace(path, std::move(context));
}
// Count ref counts from compilation contexts.
for(auto entry: *root->compilation_contexts()) {
index.canonical_ref_counts[entry->canonical_id()] += 1;
}
// Deserialize removed bitmap.
if(root->removed() && root->removed()->size() > 0) {
index.removed = read_bitmap(root->removed());
}
for(auto entry: *root->occurrences()) {
index.occurrences.try_emplace(*safe_cast<Occurrence>(entry->occurrence()),
read_bitmap(entry->context()));
}
for(auto entry: *root->relations()) {
auto& relations = index.relations[entry->symbol()];
for(auto relation_entry: *entry->relations()) {
relations.try_emplace(*safe_cast<Relation>(relation_entry->relation()),
read_bitmap(relation_entry->context()));
};
for(auto& [_, context]: index.header_contexts) {
for(auto& include: context.includes) {
count_ref(include.canonical_id);
}
}
if(root->content()) {
index.content = root->content()->str();
for(auto& [_, context]: index.compilation_contexts) {
count_ref(context.canonical_id);
}
// Rebuild the derived reference counts from the decoded contexts. Ids are
// bounds-checked: the verifier guarantees structure, not semantics, so a
// corrupted-but-well-formed shard must not index out of range.
// Bound the allocation by the maximum id actually referenced by the
// decoded contexts (clamped to max_canonical_id), rather than trusting
// max_canonical_id directly — a corrupted-but-structurally-valid shard
// must not be able to force an unbounded allocation here.
std::uint32_t max_ref = 0;
for(auto& [_, context]: index.header_contexts) {
for(auto& include: context.includes) {
max_ref = std::max(max_ref, include.canonical_id);
}
}
for(auto& [_, context]: index.compilation_contexts) {
max_ref = std::max(max_ref, context.canonical_id);
}
auto safe_size = std::min<std::uint64_t>(index.max_canonical_id,
static_cast<std::uint64_t>(max_ref) + 1);
index.canonical_ref_counts.resize(safe_size, 0);
auto count_ref = [&](std::uint32_t canonical_id) {
if(canonical_id < index.canonical_ref_counts.size()) {
index.canonical_ref_counts[canonical_id] += 1;
}
};
for(auto& [_, context]: index.header_contexts) {
for(auto& include: context.includes) {
count_ref(include.canonical_id);
}
}
for(auto& [_, context]: index.compilation_contexts) {
count_ref(context.canonical_id);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index/merged_index.cpp` around lines 356 - 372, The rebuild of
MergedIndex::canonical_ref_counts is trusting the decoded max_canonical_id
field, which can cause an excessive allocation from a corrupted but structurally
valid shard. In MergedIndex::load_in_memory (or the nearby rebuild block),
compute the needed size from the actual canonical ids referenced in
header_contexts and compilation_contexts, then clamp or validate before calling
resize; keep the existing bounds-checked count_ref logic but avoid using
index.max_canonical_id as the direct allocation size. Use the surrounding
load/verify flow in MergedIndex to ensure the derived count array is sized only
to what is semantically required.

Comment thread src/index/serialization.h Outdated
Comment on lines +146 to +162
template <typename Vis, typename Config>
struct deserialize_visit<Vis, clice::index::PathPool, Config> {
static bool visit(Vis& vis, clice::index::PathPool& out) {
std::vector<std::string> paths;
if(!decode_value<Config>(vis, paths)) {
return false;
}
out.paths.clear();
out.cache.clear();
for(const auto& path: paths) {
if(!path.empty()) {
out.path_id(path);
}
}
return true;
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Skipping empty paths silently shifts subsequent path ids.

path_id() assigns ids by insertion order (paths.size() at call time), so skipping an empty entry mid-list shifts every later path's id down by one relative to how it was originally serialized. Any stored references that key off the original path id (e.g. path_file_indices, symbol reference sets) would then resolve to the wrong path after reload — a silent data-integrity issue rather than the "never misread" failure mode the rest of this migration aims for.

Since PathPool::path_id asserts !path.empty(), an empty entry can only appear here due to blob corruption; treat it as a decode failure instead of quietly dropping it.

🛡️ Proposed fix
         out.paths.clear();
         out.cache.clear();
         for(const auto& path: paths) {
-            if(!path.empty()) {
-                out.path_id(path);
-            }
+            if(path.empty()) {
+                return false;
+            }
+            out.path_id(path);
         }
         return true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
template <typename Vis, typename Config>
struct deserialize_visit<Vis, clice::index::PathPool, Config> {
static bool visit(Vis& vis, clice::index::PathPool& out) {
std::vector<std::string> paths;
if(!decode_value<Config>(vis, paths)) {
return false;
}
out.paths.clear();
out.cache.clear();
for(const auto& path: paths) {
if(!path.empty()) {
out.path_id(path);
}
}
return true;
}
};
template <typename Vis, typename Config>
struct deserialize_visit<Vis, clice::index::PathPool, Config> {
static bool visit(Vis& vis, clice::index::PathPool& out) {
std::vector<std::string> paths;
if(!decode_value<Config>(vis, paths)) {
return false;
}
out.paths.clear();
out.cache.clear();
for(const auto& path: paths) {
if(path.empty()) {
return false;
}
out.path_id(path);
}
return true;
}
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index/serialization.h` around lines 146 - 162, The PathPool
deserialization in deserialize_visit<Vis, clice::index::PathPool, Config>::visit
is silently dropping empty strings, which changes later path_id() assignments
and can corrupt id-based references. Update the loop so an empty entry is
treated as a decode failure: detect it while rebuilding out.paths/cache and
return false instead of skipping it, keeping the PathPool state consistent with
the original serialized ordering.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
cmake/package.cmake (1)

33-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider pinning to a full commit SHA instead of an abbreviated one.

GIT_TAG 5882279 is a short hash rather than a tag or full 40-character SHA. Since GIT_SHALLOW isn't set for this fetch, CMake will do a full clone and resolve it locally, so this should work, but a full SHA is unambiguous and more resilient to future object-name collisions or tooling that expects exact SHAs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmake/package.cmake` at line 33, The fetch in package.cmake uses an
abbreviated GIT_TAG, so update the Git tag reference in the relevant
FetchContent or ExternalProject declaration to use the full 40-character commit
SHA instead of 5882279. Keep the same target revision, but make it unambiguous
and stable by replacing the short hash in the GIT_TAG field.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cmake/package.cmake`:
- Line 33: The fetch in package.cmake uses an abbreviated GIT_TAG, so update the
Git tag reference in the relevant FetchContent or ExternalProject declaration to
use the full 40-character commit SHA instead of 5882279. Keep the same target
revision, but make it unambiguous and stable by replacing the short hash in the
GIT_TAG field.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 16ff3203-30ee-46c2-ab05-a574cbe28b66

📥 Commits

Reviewing files that changed from the base of the PR and between 6dd84b5 and 2a59f23.

📒 Files selected for processing (1)
  • cmake/package.cmake

… ProjectIndex

- The Builder now writes each occurrence straight into path_file_indices (or
  main_file_index for the interested file) instead of a FileID-keyed scratch
  map that build() drained afterwards. TUIndex::file_indices is gone, and a
  path included under several FileIDs now merges its contributions instead
  of keeping an arbitrary last one. Tests drop their fid-based helpers and
  iterate path_file_indices like production code does.
- IncludeGraph::first_include_of names the "first include location of a
  path" scan the background indexer already did inline; tests reuse it.
- ProjectIndex is its own serialization root: format_version / paths /
  shards become real (runtime-empty) fields, serialize encodes a persisted
  twin of the same type, and from() decodes into self before rebasing
  bitmaps onto the running pool. The intermediate PersistedProjectIndex
  type is gone. index_format_version moves to index/shared.h so the header
  no longer needs the codec.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/index/serialization.h (2)

67-98: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use readSafe for Roaring deserialization
roaring::Roaring::read(..., false) can read past the end of bytes on malformed input. Switch to readSafe(..., bytes.size(), false) so deserialization stays bounded by the buffer length.

🛡️ Proposed fix
         if(bytes.empty()) {
             out = roaring::Roaring();
         } else {
-            out = roaring::Roaring::read(reinterpret_cast<const char*>(bytes.data()), false);
+            out = roaring::Roaring::readSafe(reinterpret_cast<const char*>(bytes.data()),
+                                              bytes.size(),
+                                              false);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index/serialization.h` around lines 67 - 98, Roaring deserialization in
deserialize_visit::visit currently uses roaring::Roaring::read(..., false),
which is not bounded by the decoded buffer and can overread on malformed input.
Update this path to use readSafe with the bytes buffer and its size, while
keeping the empty-bytes case unchanged, so the deserialization of
roaring::Roaring stays safely constrained to the input vector.

24-65: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Decoded enum values aren't range-checked.

deserialize_visit<..., clice::RelationKind, ...> casts any decoded std::uint32_t straight into RelationKind::Kind without checking it falls within the valid Invalid..Callee range (0-16). The same pattern applies to the SymbolKind specialization further down (decoding an arbitrary std::uint8_t). A corrupted or truncated blob (schema verification only checks structural shape, not semantic enum ranges) will silently produce an enum holding a value that matches none of the named cases, which can then flow through is_one_of/switch-style logic elsewhere with no error surfaced. Given the PR's stated goal of not misreading corrupted blobs (and the sibling PathPool deserializer already treats bad data as a hard failure), consider bounds-checking here too.

🛡️ Proposed fix for RelationKind
     static bool visit(Vis& vis, clice::RelationKind& out) {
         std::uint32_t value = 0;
         if(!decode_value<Config>(vis, value)) {
             return false;
         }
+        if(value > clice::RelationKind::Callee) {
+            return false;
+        }
         out = clice::RelationKind(static_cast<clice::RelationKind::Kind>(value));
         return true;
     }

Please confirm SymbolKind's valid range and whether an out-of-range value could ever be used for indexing (e.g. in IndexQuery::to_lsp_symbol_kind or similar lookup tables), which would raise the severity from a data-quality issue to an OOB read.

#!/bin/bash
# Inspect SymbolKind's enum definition and any array/table indexing by its value.
fd -HI 'symbol_kind' 
rg -n -C5 'enum.*SymbolKind' 
rg -n -C5 'SymbolKind::' --type=cpp -g '*.cpp' | rg -n 'value\(\)|static_cast'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index/serialization.h` around lines 24 - 65, The enum deserializers for
clice::RelationKind and clice::SymbolKind currently accept any decoded integer
and cast it directly into the enum, so add explicit range validation in
deserialize_visit for both specializations before assigning to out. Use the
existing valid enum bounds for RelationKind::Kind and confirm SymbolKind::Kind’s
allowed range from its definition, then return false on out-of-range values
instead of constructing the enum. Keep the change localized to the
deserialize_visit helpers in serialization.h so corrupted blobs fail hard before
invalid enum values reach switch logic or any table indexing.
🧹 Nitpick comments (1)
tests/unit/index/merged_index_tests.cpp (1)

27-34: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

file_index_of uses operator[], which can silently insert a default entry.

If fid's path id isn't actually present in path_file_indices (e.g. a test setup mistake), this quietly default-constructs an empty FileIndex instead of failing with a clear error, making failures harder to diagnose.

Suggested fix using explicit lookup
 index::FileIndex& file_index_of(index::TUIndex& index, clang::FileID fid) {
     if(fid == unit->interested_file()) {
         return index.main_file_index;
     }
-    return index.path_file_indices[index.graph.path_id(fid)];
+    auto it = index.path_file_indices.find(index.graph.path_id(fid));
+    ASSERT_TRUE(it != index.path_file_indices.end());
+    return it->second;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/index/merged_index_tests.cpp` around lines 27 - 34, The issue is
that file_index_of uses operator[] on index.path_file_indices, which can
silently create a default FileIndex when the path id is missing. Update
file_index_of to use an explicit lookup on path_file_indices and fail fast when
index.graph.path_id(fid) is not present, so test setup mistakes are reported
clearly instead of being masked by an empty entry.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/index/serialization.h`:
- Around line 67-98: Roaring deserialization in deserialize_visit::visit
currently uses roaring::Roaring::read(..., false), which is not bounded by the
decoded buffer and can overread on malformed input. Update this path to use
readSafe with the bytes buffer and its size, while keeping the empty-bytes case
unchanged, so the deserialization of roaring::Roaring stays safely constrained
to the input vector.
- Around line 24-65: The enum deserializers for clice::RelationKind and
clice::SymbolKind currently accept any decoded integer and cast it directly into
the enum, so add explicit range validation in deserialize_visit for both
specializations before assigning to out. Use the existing valid enum bounds for
RelationKind::Kind and confirm SymbolKind::Kind’s allowed range from its
definition, then return false on out-of-range values instead of constructing the
enum. Keep the change localized to the deserialize_visit helpers in
serialization.h so corrupted blobs fail hard before invalid enum values reach
switch logic or any table indexing.

---

Nitpick comments:
In `@tests/unit/index/merged_index_tests.cpp`:
- Around line 27-34: The issue is that file_index_of uses operator[] on
index.path_file_indices, which can silently create a default FileIndex when the
path id is missing. Update file_index_of to use an explicit lookup on
path_file_indices and fail fast when index.graph.path_id(fid) is not present, so
test setup mistakes are reported clearly instead of being masked by an empty
entry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b9a5b104-9af3-4a03-976d-38d500456510

📥 Commits

Reviewing files that changed from the base of the PR and between 2a59f23 and c5f30f8.

📒 Files selected for processing (10)
  • src/index/include_graph.h
  • src/index/project_index.cpp
  • src/index/project_index.h
  • src/index/serialization.h
  • src/index/shared.h
  • src/index/tu_index.cpp
  • src/index/tu_index.h
  • src/server/index/background_indexer.cpp
  • tests/unit/index/index_query_tests.cpp
  • tests/unit/index/merged_index_tests.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/index/tu_index.h

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c5f30f8a8f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +361 to 363
if(canonical_id < index.canonical_ref_counts.size()) {
index.canonical_ref_counts[canonical_id] += 1;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject invalid canonical IDs on load

When a structurally valid shard has a canonical_id greater than or equal to max_canonical_id, this silently ignores it while leaving the bad header/compilation context in the index. The next reindex or removal of that context calls release_canonical with the same id and indexes canonical_ref_counts[canonical_id] out of bounds, so a corrupted current-format cache can still crash the server despite passing load() verification; reject the shard or drop the invalid context instead of just skipping the count.

Useful? React with 👍 / 👎.

Comment thread src/index/project_index.cpp Outdated
std::span<const std::uint8_t> bytes(static_cast<const std::uint8_t*>(data), size);

ProjectIndex loaded;
if(auto result = kota::codec::fbs::from_flatbuffer(bytes, loaded); !result) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check ProjectIndex version before full decode

For a ProjectIndex blob that still has the kotatsu FlatBuffer identifier but carries an old format_version, this decodes the entire object — including Roaring bitmaps via Roaring::read — before the version check below can return nullopt. A stale or corrupted cache with malformed bitmap bytes can therefore abort during startup instead of being discarded as intended; read format_version from a verified table view first, like MergedIndex::load, and only then run from_flatbuffer.

Useful? React with 👍 / 👎.

/// Ordering by raw kind value; lets Relation default its own operator<=> so
/// serialized relation maps have a deterministic, binary-searchable order.
constexpr auto operator<=>(RelationKind lhs, RelationKind rhs) {
return lhs.value() <=> rhs.value();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include for the spaceship operator

When this header is included without another standard header that happens to pull in <compare> first, this <=> expression fails to compile because std::strong_ordering is not declared. Since relation_kind.h is directly included from several public/internal headers, an include-order change or a standalone include can break the build; add the required <compare> include here.

Useful? React with 👍 / 👎.

…ed helpers

- Codec adapters collapse to one value-mode serialize_visit specialization
  per type (kotatsu's to_wire/from_wire protocol) instead of paired
  serialize/deserialize specializations.
- write_flatbuffer/read_flatbuffer helpers replace the encode/write and
  span/decode boilerplate repeated across the three index types.
- Builder::finalize sorts and dedups relations/occurrences with their
  defaulted orderings instead of hand-written tuple lambdas, and the
  sorted-occurrence walk shared by FileIndex::lookup and MergedIndex's
  in-memory path is extracted into lookup_occurrences.
- Bump kotatsu pin for the value-mode protocol.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
cmake/package.cmake (1)

30-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider pinning to full commit SHA.

5ff9bf1 is an abbreviated hash; a full 40-character SHA is more collision-resistant and easier to audit for supply-chain provenance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmake/package.cmake` around lines 30 - 34, The kotatsu dependency declaration
is using an abbreviated GIT_TAG, which should be replaced with the full
40-character commit SHA for stronger supply-chain pinning. Update the
FetchContent_Declare block for kotatsu to use the full immutable commit
identifier instead of the short hash so the dependency is easier to audit and
less collision-prone.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/index/serialization.h`:
- Around line 108-113: The from_wire helper in serialization.h is using
Roaring::read() on raw bytes without bounds checking. Update the from_wire
function to decode with Roaring::readSafe(..., bytes.size(), false) instead,
preserving the existing false native-format behavior while ensuring the buffer
is length-bounded.

---

Nitpick comments:
In `@cmake/package.cmake`:
- Around line 30-34: The kotatsu dependency declaration is using an abbreviated
GIT_TAG, which should be replaced with the full 40-character commit SHA for
stronger supply-chain pinning. Update the FetchContent_Declare block for kotatsu
to use the full immutable commit identifier instead of the short hash so the
dependency is easier to audit and less collision-prone.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c4531207-539d-43f8-96a6-2082450813c4

📥 Commits

Reviewing files that changed from the base of the PR and between c5f30f8 and d6d7513.

📒 Files selected for processing (6)
  • cmake/package.cmake
  • src/index/merged_index.cpp
  • src/index/project_index.cpp
  • src/index/serialization.h
  • src/index/tu_index.cpp
  • src/index/tu_index.h
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/index/tu_index.cpp
  • src/index/merged_index.cpp

Comment thread src/index/serialization.h
Comment on lines +108 to +113
static roaring::Roaring from_wire(const wire_type& bytes) {
if(bytes.empty()) {
return roaring::Roaring();
}
return roaring::Roaring::read(reinterpret_cast<const char*>(bytes.data()), false);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

roaring Roaring readSafe vs read non-portable bounds checking maxbytes

💡 Result:

In CRoaring, the distinction between "safe" and standard ("unsafe") deserialization functions centers on bounds checking and memory safety when processing serialized input [1][2]. Safe Deserialization (readSafe /..._safe variants) The functions ending in _safe (e.g., roaring_bitmap_portable_deserialize_safe and the C++ wrapper Roaring::readSafe) accept a maxbytes parameter [3][4]. They are guaranteed to not read beyond this provided buffer limit, preventing potential buffer overflows [1][5][4]. If the data is truncated or too small for a valid bitmap, the function safely returns NULL [1][4]. Standard/Unsafe Deserialization Standard deserialization functions (e.g., roaring_bitmap_portable_deserialize or the base Roaring::read) do not take a length argument [1][2]. They assume the input buffer points to a complete, correctly formatted serialized bitmap [1]. If the input is invalid, malformed, or shorter than the actual bitmap, these functions may perform out-of-bounds reads, potentially leading to crashes or security vulnerabilities [1][2]. Key Considerations: - Compatibility: The "safe" variants are designed to be compatible with the standard Roaring format specification used by Java and Go implementations [1][4]. - Untrusted Input: Because "safe" functions only guarantee memory safety (not logic safety), they do not verify that the data itself is a valid bitmap [5][4]. If an attacker provides "garbage" data that passes the length check but violates the format specification (e.g., incorrect container ordering), the resulting bitmap may be in an invalid state [5][4]. For untrusted input, it is recommended to call the internal validation functions (e.g., roaring_bitmap_internal_validate) after deserialization to ensure the bitmap is safe to use [5][2]. - Non-portable Deserialization: While the query mentions "non-portable" in the context of bounds checking, note that the library also provides "portable" vs "non-portable" (or "native") serialization formats. The "safe" deserialization functions specifically support the portable format [1][4][2]. Using non-portable (native) formats is generally not recommended for cross-platform data exchange or long-term storage [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File outline ==\n'
ast-grep outline src/index/serialization.h --view expanded || true

printf '\n== Relevant lines ==\n'
sed -n '90,130p' src/index/serialization.h

printf '\n== Search for Roaring API references ==\n'
rg -n "readSafe|Roaring::read\\(|roaring::Roaring" src include . -g '!**/node_modules/**' || true

printf '\n== Search for roaring headers in repo ==\n'
fd -a 'roaring*.h' . || true
fd -a 'roaring*.hh' . || true

Repository: clice-io/clice

Length of output: 1951


🌐 Web query:

CRoaring C++ Roaring::readSafe signature non-portable portable deserialize bounds checking

💡 Result:

In the CRoaring C++ library, Roaring::readSafe is the recommended method for deserializing bitmaps from potentially untrusted or external sources, as it provides critical memory safety guarantees by preventing buffer overflows [1][2]. The function signature for Roaring::readSafe is defined as: static Roaring readSafe(const char* buf, size_t maxbytes, bool portable = true); Key aspects of its functionality include: 1. Bounds Checking: Unlike the standard read method, readSafe enforces memory safety by ensuring the deserialization process does not read beyond the specified maxbytes [1][3][2]. This protects the application against buffer overflow vulnerabilities when processing external data [4][3]. 2. Portable vs. Non-portable: The portable parameter defaults to true [2]. When set to true, the library uses a format compatible with other Roaring Bitmap implementations (e.g., Java and Go) [5][3]. Setting it to false enables a custom, non-portable format that can be more space-efficient, especially for very sparse bitmaps, but it is not intended for cross-language interoperability [5]. 3. Safety vs. Validity: While readSafe prevents buffer overflows (memory safety), it does not automatically guarantee that the resulting bitmap is logically valid or consistent [5][3]. It assumes the input data was once serialized from a valid bitmap according to the Roaring format specification [5][3]. If the input contains garbage or is malformed, the bitmap may be initialized in an invalid state [5][3]. To fully validate that the deserialized bitmap is safe for use from untrusted sources, users should call roaring_bitmap_internal_validate on the resulting structure [4][3]. Additionally, the library recommends using checksums at the application level to ensure data integrity, as CRoaring does not perform internal checksumming [1][4]. 4. Endianness: The portable format is endian-sensitive [3]. On big-endian systems, the serialized data will also be big-endian, which may cause incompatibility if data is transferred between systems with different endianness [3].

Citations:


Use readSafe(..., bytes.size(), false) here
Roaring::read() leaves the buffer unchecked; switch to readSafe() to bound the decode, but keep false so this native wire format still round-trips.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index/serialization.h` around lines 108 - 113, The from_wire helper in
serialization.h is using Roaring::read() on raw bytes without bounds checking.
Update the from_wire function to decode with Roaring::readSafe(...,
bytes.size(), false) instead, preserving the existing false native-format
behavior while ensuring the buffer is length-bounded.

@16bit-ykiko

Copy link
Copy Markdown
Member Author

Superseded — kotatsu reflection has been adopted via #598.

@16bit-ykiko
16bit-ykiko deleted the migrate/flatbuffers-to-kotatsu-reflection-v2 branch August 27, 2026 06:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant