From d1cb29667191f77b82f71d320c25dbeffc854eca Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Tue, 30 Jun 2026 09:58:27 +0200 Subject: [PATCH 1/3] statedb: validate primary index uniqueness earlier in NewTableAny Validate that the primary index is unique before starting to construct any objects. Nothing in the check depends on these being present. Signed-off-by: Tobias Klauser --- table.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/table.go b/table.go index c3b6dcd..0482f19 100644 --- a/table.go +++ b/table.go @@ -80,6 +80,11 @@ func NewTableAny[Obj any]( return nil, err } + // Primary index must always be unique + if !primaryIndexer.isUnique() { + return nil, tableError(tableName, ErrPrimaryIndexNotUnique) + } + toAnyIndexer := func(idx Indexer[Obj], pos int) anyIndexer { return anyIndexer{ name: idx.indexName(), @@ -117,11 +122,6 @@ func NewTableAny[Obj any]( indexPos++ } - // Primary index must always be unique - if !primaryIndexer.isUnique() { - return nil, tableError(tableName, ErrPrimaryIndexNotUnique) - } - // Validate that indexes have unique ids. indexNames := map[string]struct{}{} indexNames[primaryIndexer.indexName()] = struct{}{} From a98887033a648809db5ed4bc04656e1f553461bb Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Tue, 30 Jun 2026 09:59:44 +0200 Subject: [PATCH 2/3] statedb: reject secondary indexes with empty name Suggested-by: Fabio Falzoi Signed-off-by: Tobias Klauser --- db_test.go | 9 +++++++++ errors.go | 3 +++ table.go | 3 +++ 3 files changed, 15 insertions(+) diff --git a/db_test.go b/db_test.go index 2259c3f..0109c1c 100644 --- a/db_test.go +++ b/db_test.go @@ -1491,6 +1491,15 @@ func Test_validateTableName(t *testing.T) { } } +func Test_validateSecondaryIndexName(t *testing.T) { + db := New() + emptyNameIndex := tagsIndex + emptyNameIndex.Name = "" + + _, err := NewTable(db, "test", idIndex, emptyNameIndex) + require.ErrorIs(t, err, ErrEmptySecondaryIndexName) +} + func Test_getAcquiredInfo(t *testing.T) { t.Parallel() db, table, _ := newTestDB(t) diff --git a/errors.go b/errors.go index 8afae05..38a405f 100644 --- a/errors.go +++ b/errors.go @@ -20,6 +20,9 @@ var ( // ErrPrimaryIndexNotUnique indicates that the primary index for the table is not marked unique. ErrPrimaryIndexNotUnique = errors.New("primary index not unique") + // ErrEmptySecondaryIndexName indicates that a secondary index for the table has an empty name. + ErrEmptySecondaryIndexName = errors.New("secondary index name is empty") + // ErrDuplicateIndex indicates that the table has two or more indexers that share the same name. ErrDuplicateIndex = errors.New("index name already in use") diff --git a/table.go b/table.go index 0482f19..ae31d33 100644 --- a/table.go +++ b/table.go @@ -116,6 +116,9 @@ func NewTableAny[Obj any]( indexPos := SecondaryIndexStartPos for _, indexer := range secondaryIndexers { name := indexer.indexName() + if name == "" { + return nil, tableError(tableName, ErrEmptySecondaryIndexName) + } anyIndexer := toAnyIndexer(indexer, indexPos) table.secondaryAnyIndexers = append(table.secondaryAnyIndexers, anyIndexer) table.indexPositions[indexPos] = name From 7ded5323c493f3fb9709b5743dee67840c467b4d Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Tue, 30 Jun 2026 10:01:29 +0200 Subject: [PATCH 3/3] statedb: handle empty index name in queries Querying a table with an empty index name currently panics with an "index out of range" runtime error. This is because (*genTable[Obj]).indexPos reads name[0] without a length check. This is reachable from normal API usage: NetIPPrefixIndex.QueryFromObject and LPMIndex.QueryFromObject return a zero-value Query (with an empty index name) when the object yields no prefixes or keys, and passing such a query to Get, List, Prefix or LowerBound panics. Fix this by treating an empty index name as a reference to the primary index in indexPos, following the existing behavior in getIndexer. Signed-off-by: Tobias Klauser --- db_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ table.go | 5 +++++ 2 files changed, 46 insertions(+) diff --git a/db_test.go b/db_test.go index 0109c1c..7aada39 100644 --- a/db_test.go +++ b/db_test.go @@ -1386,6 +1386,47 @@ func TestDB_EmptyKeys(t *testing.T) { } +func TestDB_EmptyIndexName(t *testing.T) { + t.Parallel() + + // A prefix index whose FromObject yields no prefixes, so that + // QueryFromObject returns a zero-value Query{} with an empty index name. + emptyPrefixIndex := NetIPPrefixIndex[*testObject]{ + Name: "empty-prefix", + Unique: true, + FromObject: func(obj *testObject) iter.Seq[netip.Prefix] { + return func(yield func(netip.Prefix) bool) {} + }, + } + + db, table := newTestDBWithMetrics(t, &NopMetrics{}, emptyPrefixIndex) + + txn := db.WriteTxn(table) + _, _, err := table.Insert(txn, &testObject{ID: 1}) + require.NoError(t, err, "Insert") + txn.Commit() + + // QueryFromObject returns an empty-index query when the object yields no + // keys. Querying with it must fall back to the primary index instead of + // panicking. + q := emptyPrefixIndex.QueryFromObject(&testObject{ID: 1}) + rtxn := db.ReadTxn() + require.NotPanics(t, func() { + table.Get(rtxn, q) + for range table.List(rtxn, q) { + } + for range table.Prefix(rtxn, q) { + } + for range table.LowerBound(rtxn, q) { + } + }) + + // An explicitly empty-index query resolves to the primary index. + obj, _, ok := table.Get(rtxn, Query[*testObject]{key: index.Uint64(1)}) + require.True(t, ok, "Get") + require.Equal(t, uint64(1), obj.ID) +} + func TestWriteJSON(t *testing.T) { t.Parallel() diff --git a/table.go b/table.go index ae31d33..e5103ff 100644 --- a/table.go +++ b/table.go @@ -202,6 +202,11 @@ func (t *genTable[Obj]) released() { } func (t *genTable[Obj]) indexPos(name string) int { + // An empty index name refers to the primary index, matching getIndexer. + if name == "" { + return PrimaryIndexPos + } + // By default don't consider the internal indexes. start := PrimaryIndexPos