mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 14:02:26 +01:00
* test: catalog GGUF models larger than the header gguf-parser-go v0.26.3+ rejects a header whose tensors declare more data than follows it. Syft parses only a 50 MB copy of each model, so any larger model is dropped. Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * fix: fix parser with rollback and verify with test Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * fix: bound GGUF tensor info before the parser reads it The header pre-scan stopped after the metadata KVs and left tensor infos to gguf-parser-go. Older releases of the library allocate whatever length a tensor name declares, so a tiny file could force a multi-GB allocation, or a fatal out of memory that panic recovery can't catch. - tensor names are bounded like metadata keys (non-empty, length limit, shared string budget) - dimension count is capped at `GGML_MAX_DIMS` - a tensor count larger than the header now fails at EOF in the pre-scan Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: give the GGUF header copy the model's real size The cataloger copies at most 50 MB of a model and parses that copy. Newer gguf-parser-go releases check the tensor data the header declares against the file size, so the truncated copy of any larger model was rejected. - after the header is validated, the copy is extended to the size the resolver reports for the model, leaving the tail as a hole the parser never reads - sources that report no size (OCI model artifacts today) keep the old behavior Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * chore(deps): restore gguf-parser-go v0.26.4 The header copy now carries the model's real size, so the pin to v0.26.0 and its dependabot ignore are no longer needed. This also brings back upstream's string and array length bounds, and the library's own `IsNumeric`. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: keep the extended GGUF header copy sparse on Windows NTFS reserves disk for the full length of a file extended with truncate unless the file is marked sparse, so cataloging a large model on Windows reserved the model's size in the temp dir until cleanup. - the header copy is marked sparse (`FSCTL_SET_SPARSE`) before it is extended - if the filesystem can't make it sparse (FAT32, exFAT), the copy is not extended, and models larger than it fail to parse as before - `golang.org/x/sys` moves from an indirect to a direct dependency Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
63 lines
2.3 KiB
Go
63 lines
2.3 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// modelSize is larger than the 50 MB syft copies from a model to parse its header, and than the tensor data
|
|
// the header fixture declares.
|
|
const modelSize = 512 << 20
|
|
|
|
// TestGGUFModelIsCataloged scans a model larger than the header copy syft parses: a real GGUF header followed
|
|
// by a sparse range standing in for the tensor data. Models under the copy size are parsed whole, so only a
|
|
// model this large catches a parser that checks the declared tensor data against the bytes it was given.
|
|
func TestGGUFModelIsCataloged(t *testing.T) {
|
|
root := t.TempDir()
|
|
header, err := os.ReadFile("testdata/models/gguf-header-only/header.gguf")
|
|
require.NoError(t, err)
|
|
modelPath := filepath.Join(root, "qwen2.5-0.5b-instruct-q2_k.gguf")
|
|
require.NoError(t, os.WriteFile(modelPath, header, 0o600))
|
|
require.NoError(t, os.Truncate(modelPath, modelSize))
|
|
|
|
out := filepath.Join(t.TempDir(), "sbom.json")
|
|
cmd, stdout, stderr := runSyft(t, nil, "scan", "dir:"+root, "-o", "syft-json="+out)
|
|
defer logOutputOnFailure(t, cmd, stdout, stderr)
|
|
assertSuccessfulReturnCode(t, stdout, stderr, cmd.ProcessState.ExitCode())
|
|
|
|
b, err := os.ReadFile(out)
|
|
require.NoError(t, err, "no SBOM written")
|
|
var doc struct {
|
|
Artifacts []struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Licenses []struct {
|
|
Value string `json:"value"`
|
|
} `json:"licenses"`
|
|
Metadata struct {
|
|
Architecture string `json:"architecture"`
|
|
Quantization string `json:"quantization"`
|
|
Parameters uint64 `json:"parameters"`
|
|
TensorCount uint64 `json:"tensorCount"`
|
|
} `json:"metadata"`
|
|
} `json:"artifacts"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(b, &doc), "SBOM is not valid JSON")
|
|
require.Len(t, doc.Artifacts, 1, "expected the model package")
|
|
|
|
model := doc.Artifacts[0]
|
|
assert.Equal(t, "qwen2.5-0.5b-instruct", model.Name)
|
|
assert.Equal(t, "model", model.Type)
|
|
require.Len(t, model.Licenses, 1)
|
|
assert.Equal(t, "apache-2.0", model.Licenses[0].Value)
|
|
assert.Equal(t, "qwen2", model.Metadata.Architecture)
|
|
assert.Equal(t, "Q2_K_L", model.Metadata.Quantization)
|
|
assert.Equal(t, uint64(290), model.Metadata.TensorCount)
|
|
assert.Equal(t, uint64(494032768), model.Metadata.Parameters)
|
|
}
|