mirror of
https://github.com/anchore/syft.git
synced 2026-05-20 04:05:24 +02:00
fix(json): use value alias in Document.UnmarshalJSON to prevent infinite recursion with encoding/json/v2 (#4748)
The pattern 'type Alias *Document' does not strip methods under encoding/json/v2 (GOEXPERIMENT=jsonv2), causing UnmarshalJSON to call itself infinitely until the goroutine stack overflows (1GB limit). Change to 'type Alias Document' with (*Alias)(d) cast — the standard Go pattern that works correctly with both encoding/json v1 and v2. Adds a regression test that uses debug.SetMaxStack to shrink the goroutine stack limit to 8MB, making the overflow happen in milliseconds rather than minutes if the recursion is reintroduced. Ref: https://github.com/golang/go/issues/75361 Signed-off-by: Benjamin Grandfond <benjamin.grandfond@docker.com>
This commit is contained in:
parent
d0ee9098cf
commit
cc3b8eb48f
@ -17,8 +17,8 @@ type Document struct {
|
||||
}
|
||||
|
||||
func (d *Document) UnmarshalJSON(data []byte) error {
|
||||
type Alias *Document
|
||||
aux := Alias(d)
|
||||
type Alias Document
|
||||
aux := (*Alias)(d)
|
||||
|
||||
if err := json.Unmarshal(data, aux); err != nil {
|
||||
return fmt.Errorf("could not unmarshal syft JSON document: %w", err)
|
||||
|
||||
@ -2,12 +2,48 @@ package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestDocumentUnmarshalJSON_NoInfiniteRecursion guards against the regression
|
||||
// where encoding/json/v2 (GOEXPERIMENT=jsonv2) would call UnmarshalJSON
|
||||
// recursively on the alias type, causing a goroutine stack overflow.
|
||||
// See: https://github.com/golang/go/issues/75361
|
||||
func TestDocumentUnmarshalJSON_NoInfiniteRecursion(t *testing.T) {
|
||||
data := `{
|
||||
"artifacts": [
|
||||
{"id": "1", "name": "pkg-a", "version": "1.0", "type": "npm", "foundBy": "cataloger", "locations": [], "licenses": [], "language": "javascript", "cpes": [], "purl": "pkg:npm/pkg-a@1.0"},
|
||||
{"id": "2", "name": "pkg-b", "version": "2.0", "type": "gem", "foundBy": "cataloger", "locations": [], "licenses": [], "language": "ruby", "cpes": [], "purl": "pkg:gem/pkg-b@2.0"}
|
||||
],
|
||||
"schema": {"version": "16.0.0", "url": "https://example.com"},
|
||||
"descriptor": {"name": "syft", "version": "1.0.0"}
|
||||
}`
|
||||
|
||||
// Shrink the max goroutine stack to 8MB so that infinite recursion
|
||||
// (golang/go#75361 — encoding/json/v2 re-dispatching to UnmarshalJSON
|
||||
// via type Alias *Document) overflows quickly rather than after minutes.
|
||||
old := debug.SetMaxStack(8 * 1024 * 1024)
|
||||
defer debug.SetMaxStack(old)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
var doc Document
|
||||
done <- json.Unmarshal([]byte(data), &doc)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
require.NoError(t, err)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("json.Unmarshal did not complete — likely infinite recursion in UnmarshalJSON")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentUnmarshalJSON_SchemaDetection(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user