Integer overflow in chunking helper causes dispatching to miss elements or panic
Description
SpiceDB is an open source, Google Zanzibar-inspired database for creating and managing security-critical application permissions. Integer overflow in chunking helper causes dispatching to miss elements or panic. Any SpiceDB cluster with any schema where a resource being checked has more than 65535 relationships for the same resource and subject type is affected by this problem. The CheckPermission, BulkCheckPermission, and LookupSubjects API methods are affected. This vulnerability is fixed in 1.29.2.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/authzed/spicedbGo | < 1.29.2 | 1.29.2 |
Affected products
1Patches
1ef443c442b96Merge pull request from GHSA-h3m7-rqc4-7h9p
3 files changed · +159 −7
internal/dispatch/graph/dispatch_test.go+99 −0 added@@ -0,0 +1,99 @@ +package graph + +import ( + "fmt" + "math" + "testing" + + "github.com/authzed/spicedb/internal/dispatch" + "github.com/authzed/spicedb/internal/graph" + core "github.com/authzed/spicedb/pkg/proto/core/v1" + v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" + "github.com/authzed/spicedb/pkg/tuple" + + "github.com/stretchr/testify/require" +) + +func TestDispatchChunking(t *testing.T) { + schema := ` + definition user { + relation self: user + } + + definition res { + relation owner : user + permission view = owner->self + }` + + resources := make([]*core.RelationTuple, 0, math.MaxUint16+1) + enabled := make([]*core.RelationTuple, 0, math.MaxUint16+1) + for i := 0; i < math.MaxUint16+1; i++ { + resources = append(resources, tuple.Parse(fmt.Sprintf("res:res1#owner@user:user%d", i))) + enabled = append(enabled, tuple.Parse(fmt.Sprintf("user:user%d#self@user:user%d", i, i))) + } + + ctx, dispatcher, revision := newLocalDispatcherWithSchemaAndRels(t, schema, append(enabled, resources...)) + + t.Run("check", func(t *testing.T) { + for _, tpl := range resources[:1] { + checkResult, err := dispatcher.DispatchCheck(ctx, &v1.DispatchCheckRequest{ + ResourceRelation: RR(tpl.ResourceAndRelation.Namespace, "view"), + ResourceIds: []string{tpl.ResourceAndRelation.ObjectId}, + ResultsSetting: v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT, + Subject: ONR(tpl.Subject.Namespace, tpl.Subject.ObjectId, graph.Ellipsis), + Metadata: &v1.ResolverMeta{ + AtRevision: revision.String(), + DepthRemaining: 50, + }, + }) + + require.NoError(t, err) + require.NotNil(t, checkResult) + require.NotEmpty(t, checkResult.ResultsByResourceId, "expected membership for resource %s", tpl.ResourceAndRelation.ObjectId) + require.Equal(t, v1.ResourceCheckResult_MEMBER, checkResult.ResultsByResourceId[tpl.ResourceAndRelation.ObjectId].Membership) + } + }) + + t.Run("lookup-resources", func(t *testing.T) { + for _, tpl := range resources[:1] { + stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResourcesResponse](ctx) + err := dispatcher.DispatchLookupResources(&v1.DispatchLookupResourcesRequest{ + ObjectRelation: RR(tpl.ResourceAndRelation.Namespace, "view"), + Subject: ONR(tpl.Subject.Namespace, tpl.Subject.ObjectId, graph.Ellipsis), + Metadata: &v1.ResolverMeta{ + AtRevision: revision.String(), + DepthRemaining: 50, + }, + OptionalLimit: veryLargeLimit, + }, stream) + + require.NoError(t, err) + + foundResources, _, _, _ := processResults(stream) + require.Len(t, foundResources, 1) + } + }) + + t.Run("lookup-subjects", func(t *testing.T) { + for _, tpl := range resources[:1] { + stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupSubjectsResponse](ctx) + + err := dispatcher.DispatchLookupSubjects(&v1.DispatchLookupSubjectsRequest{ + ResourceRelation: RR(tpl.ResourceAndRelation.Namespace, "view"), + ResourceIds: []string{tpl.ResourceAndRelation.ObjectId}, + SubjectRelation: RR(tpl.Subject.Namespace, graph.Ellipsis), + Metadata: &v1.ResolverMeta{ + AtRevision: revision.String(), + DepthRemaining: 50, + }, + }, stream) + + require.NoError(t, err) + res := stream.Results() + require.Len(t, res, 1) + require.Len(t, res[0].FoundSubjectsByResourceId, 1) + require.NotNil(t, res[0].FoundSubjectsByResourceId["res1"]) + require.Len(t, res[0].FoundSubjectsByResourceId["res1"].FoundSubjects, math.MaxUint16+1) + } + }) +}
pkg/genutil/slicez/chunking.go+7 −5 modified@@ -18,11 +18,12 @@ func ForEachChunkUntil[T any](data []T, chunkSize uint16, handler func(items []T chunkSize = 1 } - dataLength := uint16(len(data)) - chunkCount := (dataLength / chunkSize) + 1 - for chunkIndex := uint16(0); chunkIndex < chunkCount; chunkIndex++ { - chunkStart := chunkIndex * chunkSize - chunkEnd := (chunkIndex + 1) * chunkSize + dataLength := uint64(len(data)) + chunkSize64 := uint64(chunkSize) + chunkCount := (dataLength / chunkSize64) + 1 + for chunkIndex := uint64(0); chunkIndex < chunkCount; chunkIndex++ { + chunkStart := chunkIndex * chunkSize64 + chunkEnd := (chunkIndex + 1) * chunkSize64 if chunkEnd > dataLength { chunkEnd = dataLength } @@ -38,5 +39,6 @@ func ForEachChunkUntil[T any](data []T, chunkSize uint16, handler func(items []T } } } + return true, nil }
pkg/genutil/slicez/chunking_test.go+53 −2 modified@@ -2,23 +2,28 @@ package slicez import ( "fmt" + "math" "testing" "github.com/stretchr/testify/require" ) func TestForEachChunk(t *testing.T) { + t.Parallel() + for _, datasize := range []int{0, 1, 5, 10, 50, 100, 250} { datasize := datasize for _, chunksize := range []uint16{1, 2, 3, 5, 10, 50} { chunksize := chunksize t.Run(fmt.Sprintf("test-%d-%d", datasize, chunksize), func(t *testing.T) { - data := []int{} + t.Parallel() + + data := make([]int, 0, datasize) for i := 0; i < datasize; i++ { data = append(data, i) } - found := []int{} + found := make([]int, 0, datasize) ForEachChunk(data, chunksize, func(items []int) { found = append(found, items...) require.True(t, len(items) <= int(chunksize)) @@ -29,3 +34,49 @@ func TestForEachChunk(t *testing.T) { } } } + +func TestForEachChunkOverflowPanic(t *testing.T) { + t.Parallel() + + datasize := math.MaxUint16 + chunksize := uint16(50) + data := make([]int, 0, datasize) + for i := 0; i < datasize; i++ { + data = append(data, i) + } + + found := make([]int, 0, datasize) + ForEachChunk(data, chunksize, func(items []int) { + found = append(found, items...) + require.True(t, len(items) <= int(chunksize)) + require.True(t, len(items) > 0) + }) + + require.Equal(t, data, found) +} + +func TestForEachChunkOverflowIncorrect(t *testing.T) { + t.Parallel() + + chunksize := uint16(50) + for _, datasize := range []int{math.MaxUint16 + int(chunksize), 10_000_000} { + datasize := datasize + t.Run(fmt.Sprintf("test-%d-%d", datasize, chunksize), func(t *testing.T) { + t.Parallel() + + data := make([]int, 0, datasize) + for i := 0; i < datasize; i++ { + data = append(data, i) + } + + found := make([]int, 0, datasize) + ForEachChunk(data, chunksize, func(items []int) { + found = append(found, items...) + require.True(t, len(items) <= int(chunksize)) + require.True(t, len(items) > 0) + }) + + require.Equal(t, data, found) + }) + } +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-h3m7-rqc4-7h9pghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-27101ghsaADVISORY
- github.com/authzed/spicedb/commit/ef443c442b96909694390324a99849b0407007feghsax_refsource_MISCWEB
- github.com/authzed/spicedb/security/advisories/GHSA-h3m7-rqc4-7h9pghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.