Check projection budgets before container allocation
This commit is contained in:
@@ -159,6 +159,9 @@ Focused check: `go test ./internal/engine -run 'Mapping|Value|Projection|Clone|O
|
||||
Finding: **A04**.
|
||||
Owners: `internal/engine/projection.go`, `value_operation.go`, `projection_test.go`.
|
||||
|
||||
Status: **complete** (A04). Prior stage commits: `2736ddf`, `deb9aa5`,
|
||||
`ae3408a`. Validation passed: `GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Projection|Inline|Resolution' -count=1` and `GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -count=1`.
|
||||
|
||||
1. After charging a container node and before its bulk reservation, compare its
|
||||
minimum remaining cost with the current node allowance: at least one node per
|
||||
sequence element and one key plus one value node per map entry. Use comparisons
|
||||
|
||||
@@ -137,7 +137,9 @@ func (o *valueOperation) projectStruct(value reflect.Value, descriptor *typeDesc
|
||||
if isNilValue(memberValue) {
|
||||
continue
|
||||
}
|
||||
o.nodeCredits++
|
||||
inline, issue := o.project(memberValue, member.descriptor, path, depth)
|
||||
o.nodeCredits--
|
||||
if issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
@@ -193,6 +195,9 @@ func (o *valueOperation) projectMap(value reflect.Value, descriptor *typeDescrip
|
||||
if issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
if issue := o.checkMinimumChildren(path, value.Len(), 2); issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
result.Mapping = make([]document.Entry, 0, value.Len())
|
||||
keys := value.MapKeys()
|
||||
slices.SortFunc(keys, func(left, right reflect.Value) int {
|
||||
@@ -217,6 +222,9 @@ func (o *valueOperation) projectSequence(value reflect.Value, descriptor *typeDe
|
||||
if issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
if issue := o.checkMinimumChildren(path, value.Len(), 1); issue != nil {
|
||||
return nil, issue
|
||||
}
|
||||
result.Sequence = make([]*document.Node, 0, value.Len())
|
||||
for index := 0; index < value.Len(); index++ {
|
||||
childPath := path.Index(index)
|
||||
|
||||
101
internal/engine/projection_budget_test.go
Normal file
101
internal/engine/projection_budget_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type sliceBudgetConfig struct {
|
||||
Values []string `yaml:"values"`
|
||||
}
|
||||
|
||||
type arrayBudgetConfig struct {
|
||||
Values [1]string `yaml:"values"`
|
||||
}
|
||||
|
||||
type mapBudgetConfig struct {
|
||||
Values map[string]string `yaml:"values"`
|
||||
}
|
||||
|
||||
type inlineBudgetFields struct {
|
||||
Name string `yaml:"name"`
|
||||
}
|
||||
|
||||
type inlineStructBudgetConfig struct {
|
||||
Fields inlineBudgetFields `yaml:",inline"`
|
||||
}
|
||||
|
||||
type inlineMapBudgetConfig struct {
|
||||
Fields map[string]string `yaml:",inline"`
|
||||
}
|
||||
|
||||
type hugeZeroBudgetConfig struct {
|
||||
Values []struct{} `yaml:"values"`
|
||||
}
|
||||
|
||||
func TestProjectionChecksMinimumContainerNodes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
typeOf reflect.Type
|
||||
value any
|
||||
exact int
|
||||
}{
|
||||
{name: "slice", typeOf: reflect.TypeFor[sliceBudgetConfig](), value: sliceBudgetConfig{Values: []string{"x"}}, exact: 4},
|
||||
{name: "array", typeOf: reflect.TypeFor[arrayBudgetConfig](), value: arrayBudgetConfig{Values: [1]string{"x"}}, exact: 4},
|
||||
{name: "map", typeOf: reflect.TypeFor[mapBudgetConfig](), value: mapBudgetConfig{Values: map[string]string{"x": "y"}}, exact: 5},
|
||||
{name: "inline struct", typeOf: reflect.TypeFor[inlineStructBudgetConfig](), value: inlineStructBudgetConfig{Fields: inlineBudgetFields{Name: "x"}}, exact: 3},
|
||||
{name: "inline map", typeOf: reflect.TypeFor[inlineMapBudgetConfig](), value: inlineMapBudgetConfig{Fields: map[string]string{"x": "y"}}, exact: 3},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
schema, issue := buildSchema(test.typeOf, testSecretBinding())
|
||||
if issue != nil {
|
||||
t.Fatal(issue.message)
|
||||
}
|
||||
for _, limits := range []struct {
|
||||
name string
|
||||
nodes int
|
||||
fail bool
|
||||
}{{name: "exact", nodes: test.exact}, {name: "insufficient", nodes: test.exact - 1, fail: true}} {
|
||||
t.Run(limits.name, func(t *testing.T) {
|
||||
_, projectionIssue := projectConfiguration(schema, reflect.ValueOf(test.value), projectionLimits{depth: maxDocumentDepth, nodes: limits.nodes})
|
||||
if limits.fail {
|
||||
if projectionIssue == nil || projectionIssue.code != CodeSchemaProjectionLimit {
|
||||
t.Fatalf("projection issue = %#v, want limit failure", projectionIssue)
|
||||
}
|
||||
} else if projectionIssue != nil {
|
||||
t.Fatalf("exact-budget projection issue = %#v", projectionIssue)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectionRejectsMaximumZeroSizedSliceBeforeAllocation(t *testing.T) {
|
||||
if os.Getenv("CONFIGKIT_HUGE_ZERO_SLICE_TEST") != "" {
|
||||
schema := mustSchemaFor[hugeZeroBudgetConfig](t)
|
||||
value := hugeZeroBudgetConfig{Values: make([]struct{}, int(^uint(0)>>1))}
|
||||
_, issue := projectConfiguration(schema, reflect.ValueOf(value), defaultProjectionLimits())
|
||||
if issue == nil || issue.code != CodeSchemaProjectionLimit {
|
||||
t.Fatalf("projection issue = %#v, want a safe limit failure", issue)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
command := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestProjectionRejectsMaximumZeroSizedSliceBeforeAllocation$")
|
||||
command.Env = append(os.Environ(), "CONFIGKIT_HUGE_ZERO_SLICE_TEST=1")
|
||||
output, err := command.CombinedOutput()
|
||||
if ctx.Err() != nil {
|
||||
t.Fatalf("maximum-length zero-sized slice projection did not terminate: %s", output)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("maximum-length zero-sized slice projection failed: %v, %s", err, output)
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,12 @@ type valueVisit struct {
|
||||
// valueOperation owns one traversal's cycle and budget state and reuses the
|
||||
// current load's descriptor cache for dynamic values.
|
||||
type valueOperation struct {
|
||||
cloneMode secretCloneMode
|
||||
schema *schemaDescriptor
|
||||
limits projectionLimits
|
||||
active map[valueVisit]bool
|
||||
nodes int
|
||||
cloneMode secretCloneMode
|
||||
schema *schemaDescriptor
|
||||
limits projectionLimits
|
||||
active map[valueVisit]bool
|
||||
nodes int
|
||||
nodeCredits int
|
||||
}
|
||||
|
||||
func (o *valueOperation) dynamicDescriptor(current reflect.Type, path Path) (*typeDescriptor, *projectionIssue) {
|
||||
@@ -72,7 +73,18 @@ func descriptorContainsSecret(descriptor *typeDescriptor, visited map[*typeDescr
|
||||
|
||||
func (o *valueOperation) charge(path Path) *projectionIssue {
|
||||
o.nodes++
|
||||
if o.limits.nodes >= 0 && o.nodes > o.limits.nodes {
|
||||
if o.limits.nodes >= 0 && o.nodes-o.nodeCredits > o.limits.nodes {
|
||||
return &projectionIssue{code: CodeSchemaProjectionLimit, path: path.clone(), message: "configuration projection exceeds the node limit"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *valueOperation) checkMinimumChildren(path Path, count, nodesPerChild int) *projectionIssue {
|
||||
if o.limits.nodes < 0 {
|
||||
return nil
|
||||
}
|
||||
remaining := o.limits.nodes - (o.nodes - o.nodeCredits)
|
||||
if remaining < 0 || count > remaining/nodesPerChild {
|
||||
return &projectionIssue{code: CodeSchemaProjectionLimit, path: path.clone(), message: "configuration projection exceeds the node limit"}
|
||||
}
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user