Repository navigation
fix: check for end iterator before dereferencing in SparseStructure lookups - #96
Merged
Merged
Conversation
…ookups get_index_bounded and the non-index-map branch of get_index evaluated '*it != j' before 'it == upper', so a lookup for a column that is absent and larger than every stored column of a row dereferenced the result of std::lower_bound even when it pointed at the end of the range. On the last row that end iterator is m_ja.end(), i.e. a read one past the vector -- undefined behaviour (AddressSanitizer reports a heap-buffer-overflow at SparseStructure.h). The '|| it == upper' still yielded the correct -1, so the result was right but the read was not. Swap the operands so the end check short-circuits before the deref. The buggy path is not reachable from the Python API (the bound SparseStructure uses the index-map lookup and get_index_bounded is not exposed), so this is verified with an AddressSanitizer harness against the header. A contract test for the missing-column-on-the-last-row case is added to guard the -1 result.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
SparseStructure::get_index_boundedand the non-index-map branch ofget_indexorder the guard as:When
jis absent and larger than every stored column of a row,lower_boundreturnsupper.*itis then dereferenced beforeit == upperis tested. On the last rowupper == m_ja.end(), so this reads one element past them_javector — undefined behaviour.The
|| it == upperstill makes the function return the correct-1, so the result is right; the defect is strictly the out-of-bounds read.Fix
Swap the operands so the end check short-circuits before the dereference:
Applied in both
get_index_boundedandget_index.Verification (AddressSanitizer)
The buggy path is not reachable from the Python API — the bound
SparseStructureusesTIndexMap = true(hash-map lookup) andget_index_boundedis not exposed — so a standalone ASan harness against the real header is used. Building aSparseStructure<double, int, true, false>with 2 rows /ja = {0, 1}and querying a missing column on the last row:Before the fix:
After the fix: no error, and lookups return the expected values (missing → -1, present → correct index).
Test
A contract test (
tests/test_sparse_structure.py::test_get_index_missing_column_past_last_row) guards the-1result for a missing column past the end of the last row. Note: through the Python bindings this exercises the index-map path, so it documents/guards the contract; the out-of-bounds read itself is covered by the ASan harness above. Full suite: 43 passed.A permanent C++/AddressSanitizer test job would be a sensible follow-up (this bug class is invisible to the pytest suite).