Skip to content

uint8 and uint16 VectorIndex offsets wrap silently #335

Description

@rly

Note on provenance: This issue was found by Claude Opus 5 while reviewing #325. The reproductions and output shown below were produced by building and running the code, but I have only briefly reviewed the analysis myself and I am not an expert in this area. Please review it critically.

Impact

VectorIndex::appendData tracks the running offset into the target column as a 64 bit value, then writes it to the dataset by casting it down to whatever width the dataset was declared with. If the offset outgrows that width, the cast wraps and a smaller number is written. Both appends report Status::Success.

The result is an index that goes backwards, which is not a valid ragged index. Reading it back through AqNWB throws. Read through h5py or PyNWB the affected row simply comes back empty, because a backwards slice yields nothing, so the data loss is silent on that side.

VectorIndex::initialize explicitly accepts T_U8 and T_U16, so this is a supported configuration, not a misuse.

What happens

Create a VectorIndex with dtype T_U8, append 200 elements, then append 100 more:

appendData(200 elements)  Status = Success
appendData(100 elements)  Status = Success

Stored index[0] = 200
Stored index[1] = 44          <- static_cast<uint8_t>(300)
True cumulative offset = 300

readIndexedCellValues threw:
  VectorIndex::readIndexedCellValues: Invalid index data, currIndex (44) < prevIndex (200)

Nothing failed at write time. The corruption only surfaces on read, and only for readers that check monotonicity.

T_U16 behaves the same way past 65,535 elements.

Cause

The cumulative counter is a uint64_t member, but the write casts it to the dataset width with no range check.

Relevant code

src/nwb/hdmf/table/VectorIndex.cpp:502:

m_currentIndex += elementsAppended;

src/nwb/hdmf/table/VectorIndex.cpp:512-524, the unchecked narrowing:

if (m_dataType.type == IO::BaseDataType::Type::T_U8) {
  CellValue indexValue(static_cast<uint8_t>(m_currentIndex));
  ...
} else if (m_dataType.type == IO::BaseDataType::Type::T_U16) {
  ...

The downstream isCompatibleCellValue check does not help here, because it compares variant alternatives rather than value ranges, so the already truncated value passes.

The read side check that eventually catches it is at :276-282.

Suggested direction

Range check m_currentIndex against the dataset width before the cast and return Status::Failure with a clear message when it will not fit. Widening the dataset automatically, the way HDMF does, would be friendlier but is a much larger change and would alter the on disk dtype mid write.

The same unchecked static_cast<uint32_t> exists on the DynamicTable::addRows path at DynamicTable.cpp:539-541, so a table with more than 4.29 billion ragged elements would wrap there too. Far less likely to be hit, but worth covering in the same fix.

Notes

tests/testVectorIndex.cpp exercises T_U8 but never pushes the offset past 255, so the overflow branch is never reached.

Activity

  1. added
    category: bugerrors in the code or code behavior
    priority: mediumnon-critical problem and/or affecting only a small set of users
    topic: apiissues related to the core AqNWB API
    on Sep 3, 2026
  2. added this to the 0.5.0 milestone on Sep 3, 2026
  3. added
    priority: lowalternative solution already working and/or relevant to only specific user(s)
    and removed
    priority: mediumnon-critical problem and/or affecting only a small set of users
    on Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    category: bugerrors in the code or code behaviorpriority: lowalternative solution already working and/or relevant to only specific user(s)topic: apiissues related to the core AqNWB API

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions