mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 14:02:26 +01:00
Fix unbounded memory use decoding AI model metadata from CycloneDX (#5404)
* fix: stop unbounded recursion decoding self-referencing CycloneDX metadata SafeTensorsModelInfo and GGUFFileHeader both carry a Parts field that is a slice of their own type. When a CycloneDX component with that metadata is decoded from properties, the slice decoder tries element 0, which decodes a struct whose Parts slice tries element 0 one level deeper, and so on. The recursion never finds a base case, building longer property-name prefixes on every level, so memory grows until the process is killed (seen in grype on any machine-learning-model component written by syft). Return early from struct decoding when no property is named by the prefix or nested below it: none of the fields could be decoded anyway, and the recursion is now bounded by the depth of the properties that exist. Refs anchore/grype#3767 Signed-off-by: Vamshidher Reddy Jannapu Reddy <v.jannapu@gmail.com> * perf: look up nested CycloneDX property names in a sorted index The struct check added in the previous commit scanned every property of a component for every struct it decoded. Decoding a slice of structs, such as a package's locations, does that once per element, so a component with n properties took O(n^2): 20,000 location properties took 5.3s to decode instead of 18ms, a cost that an untrusted SBOM can choose. Sort the property names once per Decode/DecodeInto call and binary-search them instead. The result of each check is unchanged; decoding is back to O(n log n) (27ms for the same 20,000 properties). Refs anchore/grype#3767 Signed-off-by: Vamshidher Reddy Jannapu Reddy <v.jannapu@gmail.com> --------- Signed-off-by: Vamshidher Reddy Jannapu Reddy <v.jannapu@gmail.com>
This commit is contained in:
@@ -345,6 +345,73 @@ func Test_decodeComponent(t *testing.T) {
|
||||
},
|
||||
wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
|
||||
},
|
||||
{
|
||||
// regression: the self-referencing Parts field made decoding recurse without bound
|
||||
name: "decode safetensors model metadata",
|
||||
component: cyclonedx.Component{
|
||||
Name: "lib",
|
||||
Version: "UNKNOWN",
|
||||
Type: cyclonedx.ComponentTypeMachineLearningModel,
|
||||
Properties: &[]cyclonedx.Property{
|
||||
{Name: "syft:package:foundBy", Value: "safetensors-cataloger"},
|
||||
{Name: "syft:package:type", Value: "model"},
|
||||
{Name: "syft:package:metadataType", Value: "safetensors-model-info"},
|
||||
{Name: "syft:metadata:format", Value: "safetensors"},
|
||||
{Name: "syft:metadata:metadataHash", Value: "afcae99661c33f88"},
|
||||
{Name: "syft:metadata:parameters", Value: "16"},
|
||||
{Name: "syft:metadata:quantization", Value: "I8"},
|
||||
{Name: "syft:metadata:shardCount", Value: "1"},
|
||||
{Name: "syft:metadata:tensorCount", Value: "1"},
|
||||
},
|
||||
},
|
||||
wantMetadata: pkg.SafeTensorsModelInfo{
|
||||
Format: "safetensors",
|
||||
MetadataHash: "afcae99661c33f88",
|
||||
Parameters: 16,
|
||||
Quantization: "I8",
|
||||
ShardCount: 1,
|
||||
TensorCount: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "decode safetensors model metadata with parts",
|
||||
component: cyclonedx.Component{
|
||||
Name: "model",
|
||||
Type: cyclonedx.ComponentTypeMachineLearningModel,
|
||||
Properties: &[]cyclonedx.Property{
|
||||
{Name: "syft:package:type", Value: "model"},
|
||||
{Name: "syft:package:metadataType", Value: "safetensors-model-info"},
|
||||
{Name: "syft:metadata:format", Value: "safetensors"},
|
||||
{Name: "syft:metadata:parts:0:format", Value: "safetensors"},
|
||||
{Name: "syft:metadata:parts:0:tensorCount", Value: "2"},
|
||||
{Name: "syft:metadata:parts:1:tensorCount", Value: "3"},
|
||||
},
|
||||
},
|
||||
wantMetadata: pkg.SafeTensorsModelInfo{
|
||||
Format: "safetensors",
|
||||
Parts: []pkg.SafeTensorsModelInfo{
|
||||
{Format: "safetensors", TensorCount: 2},
|
||||
{TensorCount: 3},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "decode gguf model metadata",
|
||||
component: cyclonedx.Component{
|
||||
Name: "model",
|
||||
Type: cyclonedx.ComponentTypeMachineLearningModel,
|
||||
Properties: &[]cyclonedx.Property{
|
||||
{Name: "syft:package:type", Value: "model"},
|
||||
{Name: "syft:package:metadataType", Value: "gguf-file-header"},
|
||||
{Name: "syft:metadata:ggufVersion", Value: "3"},
|
||||
{Name: "syft:metadata:tensorCount", Value: "291"},
|
||||
},
|
||||
},
|
||||
wantMetadata: pkg.GGUFFileHeader{
|
||||
GGUFVersion: 3,
|
||||
TensorCount: 291,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "generate a purl from package type",
|
||||
component: cyclonedx.Component{
|
||||
|
||||
@@ -171,7 +171,7 @@ func Decode(typ reflect.Type, values map[string]string, prefix string, fn FieldN
|
||||
|
||||
v := reflect.New(typ)
|
||||
|
||||
decode(values, v, prefix, fn)
|
||||
decode(values, sortedPropertyNames(values), v, prefix, fn)
|
||||
|
||||
switch {
|
||||
case isSlice && isPtr:
|
||||
@@ -192,11 +192,11 @@ func DecodeInto(obj any, values map[string]string, prefix string, fn FieldName)
|
||||
value = value.Elem()
|
||||
}
|
||||
|
||||
decode(values, value, prefix, fn)
|
||||
decode(values, sortedPropertyNames(values), value, prefix, fn)
|
||||
}
|
||||
|
||||
//nolint:funlen,gocognit,gocyclo
|
||||
func decode(vals map[string]string, value reflect.Value, prefix string, fn FieldName) bool {
|
||||
func decode(vals map[string]string, names propertyNames, value reflect.Value, prefix string, fn FieldName) bool {
|
||||
if !value.IsValid() {
|
||||
return false
|
||||
}
|
||||
@@ -211,7 +211,7 @@ func decode(vals map[string]string, value reflect.Value, prefix string, fn Field
|
||||
if v.IsNil() {
|
||||
v = reflect.New(t)
|
||||
}
|
||||
if decode(vals, v.Elem(), prefix, fn) && value.CanSet() {
|
||||
if decode(vals, names, v.Elem(), prefix, fn) && value.CanSet() {
|
||||
o := v.Interface()
|
||||
log.Tracef("%v", o)
|
||||
value.Set(v)
|
||||
@@ -273,7 +273,7 @@ func decode(vals map[string]string, value reflect.Value, prefix string, fn Field
|
||||
newType = t.Elem()
|
||||
}
|
||||
v := reflect.New(newType)
|
||||
if decode(vals, v.Elem(), str, fn) {
|
||||
if decode(vals, names, v.Elem(), str, fn) {
|
||||
// append to slice
|
||||
if t.Kind() != reflect.Pointer {
|
||||
v = v.Elem()
|
||||
@@ -308,7 +308,8 @@ func decode(vals map[string]string, value reflect.Value, prefix string, fn Field
|
||||
newKeyType = keyType.Elem()
|
||||
}
|
||||
k := reflect.New(newKeyType)
|
||||
if !decode(keyVals, k.Elem(), key, fn) {
|
||||
// a key is decoded from the single property holding it, so it has no nested property names
|
||||
if !decode(keyVals, nil, k.Elem(), key, fn) {
|
||||
log.Debugf("unable to decode key for: %s", key)
|
||||
continue
|
||||
}
|
||||
@@ -322,7 +323,7 @@ func decode(vals map[string]string, value reflect.Value, prefix string, fn Field
|
||||
newValueType = valueType.Elem()
|
||||
}
|
||||
v := reflect.New(newValueType)
|
||||
if decode(vals, v.Elem(), key, fn) {
|
||||
if decode(vals, names, v.Elem(), key, fn) {
|
||||
if valueType.Kind() != reflect.Pointer {
|
||||
v = v.Elem()
|
||||
}
|
||||
@@ -339,6 +340,13 @@ func decode(vals map[string]string, value reflect.Value, prefix string, fn Field
|
||||
return false
|
||||
}
|
||||
case reflect.Struct:
|
||||
// every field is decoded from the property named prefix or from one nested below it, so there is
|
||||
// nothing to decode when there is none. Stopping here also bounds the recursion for a type that refers
|
||||
// to itself (e.g. a Parts []T field): otherwise decoding the first element of an absent []T decodes
|
||||
// the same []T one level deeper, without end.
|
||||
if !names.hasPrefix(prefix) {
|
||||
return false
|
||||
}
|
||||
values := false
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
f := typ.Field(i)
|
||||
@@ -349,7 +357,7 @@ func decode(vals map[string]string, value reflect.Value, prefix string, fn Field
|
||||
continue
|
||||
}
|
||||
|
||||
if decode(vals, v, name, fn) {
|
||||
if decode(vals, names, v, name, fn) {
|
||||
values = true
|
||||
}
|
||||
}
|
||||
@@ -361,6 +369,34 @@ func decode(vals map[string]string, value reflect.Value, prefix string, fn Field
|
||||
return true
|
||||
}
|
||||
|
||||
// propertyNames holds the names of the properties being decoded, sorted, so that decode can find whether a property
|
||||
// is nested below a prefix without scanning every property for every struct it decodes
|
||||
type propertyNames []string
|
||||
|
||||
func sortedPropertyNames(vals map[string]string) propertyNames {
|
||||
names := make(propertyNames, 0, len(vals))
|
||||
for name := range vals {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
// hasPrefix reports whether there is a property named prefix or any property nested below it
|
||||
func (names propertyNames) hasPrefix(prefix string) bool {
|
||||
if prefix == "" {
|
||||
return len(names) > 0
|
||||
}
|
||||
i := sort.SearchStrings(names, prefix)
|
||||
if i < len(names) && names[i] == prefix {
|
||||
return true
|
||||
}
|
||||
// names sorting between prefix and prefix+":" (e.g. "parts:10" between "parts:1" and "parts:1:") are skipped
|
||||
nested := prefix + ":"
|
||||
i += sort.SearchStrings(names[i:], nested)
|
||||
return i < len(names) && strings.HasPrefix(names[i], nested)
|
||||
}
|
||||
|
||||
func PtrToStruct(ptr any) any {
|
||||
v := reflect.ValueOf(ptr)
|
||||
if v.IsZero() && v.Type().Kind() != reflect.Struct {
|
||||
|
||||
@@ -41,6 +41,13 @@ type T5 struct {
|
||||
Map map[string]string
|
||||
}
|
||||
|
||||
// TRecursive refers to itself, like pkg.SafeTensorsModelInfo and pkg.GGUFFileHeader do through their Parts field
|
||||
type TRecursive struct {
|
||||
Name string
|
||||
Children []TRecursive
|
||||
Next *TRecursive
|
||||
}
|
||||
|
||||
func Test_EncodeDecodeCycle(t *testing.T) {
|
||||
val := 99
|
||||
|
||||
@@ -122,6 +129,33 @@ func Test_EncodeDecodeCycle(t *testing.T) {
|
||||
{"t2 elem 1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "recursive type without children",
|
||||
value: &TRecursive{
|
||||
Name: "root",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "recursive type with nested children",
|
||||
value: &TRecursive{
|
||||
Name: "root",
|
||||
Children: []TRecursive{
|
||||
{
|
||||
Name: "child 0",
|
||||
Children: []TRecursive{
|
||||
{Name: "grandchild 0"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "child 1",
|
||||
Next: &TRecursive{Name: "next of child 1"},
|
||||
},
|
||||
},
|
||||
Next: &TRecursive{
|
||||
Name: "next",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "map of strings",
|
||||
value: &T5{
|
||||
@@ -154,3 +188,56 @@ func Test_EncodeDecodeCycle(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_propertyNames_hasPrefix(t *testing.T) {
|
||||
names := sortedPropertyNames(map[string]string{
|
||||
"props:name": "root",
|
||||
"props:children:10:x": "a",
|
||||
"props:children:2:x": "b",
|
||||
"props:children:3": "c",
|
||||
"props:children:40": "d",
|
||||
"props:children:4:x": "e",
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
prefix string
|
||||
want bool
|
||||
}{
|
||||
{prefix: "", want: true},
|
||||
{prefix: "props", want: true},
|
||||
{prefix: "props:name", want: true},
|
||||
{prefix: "props:nam", want: false},
|
||||
{prefix: "props:children", want: true},
|
||||
{prefix: "props:children:0", want: false},
|
||||
// only "props:children:10:x" shares this prefix as a string
|
||||
{prefix: "props:children:1", want: false},
|
||||
{prefix: "props:children:10", want: true},
|
||||
{prefix: "props:children:2", want: true},
|
||||
{prefix: "props:children:3", want: true},
|
||||
{prefix: "props:children:3:x", want: false},
|
||||
// "props:children:40" sorts between this prefix and the nested "props:children:4:x"
|
||||
{prefix: "props:children:4", want: true},
|
||||
{prefix: "other", want: false},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.prefix, func(t *testing.T) {
|
||||
assert.Equal(t, test.want, names.hasPrefix(test.prefix))
|
||||
})
|
||||
}
|
||||
|
||||
assert.False(t, sortedPropertyNames(nil).hasPrefix(""))
|
||||
}
|
||||
|
||||
func Test_DecodeRecursiveTypeWithManyChildren(t *testing.T) {
|
||||
// more than ten children, so that "children:1" sorts next to "children:10"
|
||||
value := &TRecursive{Name: "root"}
|
||||
for i := 0; i < 12; i++ {
|
||||
value.Children = append(value.Children, TRecursive{
|
||||
Children: []TRecursive{{Name: "grandchild"}},
|
||||
})
|
||||
}
|
||||
|
||||
vals := Encode(value, "props", OptionalJSONTag)
|
||||
|
||||
assert.EqualValues(t, value, Decode(reflect.TypeOf(value), vals, "props", OptionalJSONTag))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user