fix(html): avoid size-hint overflow for MaxInt generated-bytes limit
Cap the encode buffer hint at the limit instead of limit+1, which wrapped to a negative capacity when MaxGeneratedBytes was math.MaxInt. The oracle now reports malformed golden lines instead of panicking.
- commit
- 3fe3371ef359bab4afde713a22b3fe697587a513
- author
- brice · · 2026-10-02 13:10 UTC
- parent
- 2ef4850
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +16 -3
M html/renderer.go +4 -3
| @@ -287,9 +287,10 @@ func (r *Renderer) buildUpdate(frame core.Frame, scratch []core.Cell, snapshot b | |||
| 287 | 287 | // the exact encoded size. The previous conservative estimate could reject | |
| 288 | 288 | // compact updates well below the configured limit. | |
| 289 | 289 | func encodeBoundedUpdate(update Update, limit int) ([]byte, error) { | |
| 290 | - | // The hint never exceeds limit+1, so an oversized update cannot reserve | |
| 291 | - | // its full estimate before the limit rejects it. | |
| 292 | - | encoded, err := appendUpdateJSON(make([]byte, 0, min(estimateUpdateJSONSize(update), limit+1)), update) | |
| 290 | + | // The hint never exceeds limit, so an oversized update cannot reserve its | |
| 291 | + | // full estimate before the limit rejects it. limit+1 would overflow for | |
| 292 | + | // math.MaxInt. | |
| 293 | + | encoded, err := appendUpdateJSON(make([]byte, 0, min(estimateUpdateJSONSize(update), limit)), update) | |
| 293 | 294 | if err != nil { | |
| 294 | 295 | return nil, fmt.Errorf("html: encode update: %w", err) | |
| 295 | 296 | } | |
| @@ -287,9 +287,10 @@ func (r *Renderer) buildUpdate(frame core.Frame, scratch []core.Cell, snapshot b | |||||
| 287 | // the exact encoded size. The previous conservative estimate could reject | 287 | // the exact encoded size. The previous conservative estimate could reject | ||
| 288 | // compact updates well below the configured limit. | 288 | // compact updates well below the configured limit. | ||
| 289 | func encodeBoundedUpdate(update Update, limit int) ([]byte, error) { | 289 | func encodeBoundedUpdate(update Update, limit int) ([]byte, error) { | ||
| 290 | - | // The hint never exceeds limit+1, so an oversized update cannot reserve | 290 | + | // The hint never exceeds limit, so an oversized update cannot reserve its |
| 291 | - | // its full estimate before the limit rejects it. | 291 | + | // full estimate before the limit rejects it. limit+1 would overflow for |
| 292 | - | encoded, err := appendUpdateJSON(make([]byte, 0, min(estimateUpdateJSONSize(update), limit+1)), update) | 292 | + | // math.MaxInt. |
| 293 | + | encoded, err := appendUpdateJSON(make([]byte, 0, min(estimateUpdateJSONSize(update), limit)), update) | |||
| 293 | if err != nil { | 294 | if err != nil { | ||
| 294 | return nil, fmt.Errorf("html: encode update: %w", err) | 295 | return nil, fmt.Errorf("html: encode update: %w", err) | ||
| 295 | } | 296 | } | ||
M html/renderer_test.go +9 -0
| @@ -2,6 +2,7 @@ package html | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "errors" | |
| 5 | + | "math" | |
| 5 | 6 | "os" | |
| 6 | 7 | "testing" | |
| 7 | 8 | ||
| @@ -127,6 +128,14 @@ func TestRendererEnforcesExactGeneratedByteLimit(t *testing.T) { | |||
| 127 | 128 | require.ErrorContains(t, err, "generated update is") | |
| 128 | 129 | } | |
| 129 | 130 | ||
| 131 | + | func TestRendererAcceptsMaxIntGeneratedBytesLimit(t *testing.T) { | |
| 132 | + | renderer, err := New(Options{Limits: Limits{MaxGeneratedBytes: math.MaxInt}}) | |
| 133 | + | require.NoError(t, err) | |
| 134 | + | prepared, err := renderer.Prepare(core.NewFrame(2, 1), nil, true, Cursor{}) | |
| 135 | + | require.NoError(t, err) | |
| 136 | + | require.NoError(t, prepared.Commit()) | |
| 137 | + | } | |
| 138 | + | ||
| 130 | 139 | func TestRendererUsesIncrementalDamageWithNormalCounts(t *testing.T) { | |
| 131 | 140 | frame := core.NewFrame(2, 2) | |
| 132 | 141 | renderer, err := New(Options{}) | |
| @@ -2,6 +2,7 @@ package html | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "errors" | 4 | "errors" | ||
| 5 | + | "math" | |||
| 5 | "os" | 6 | "os" | ||
| 6 | "testing" | 7 | "testing" | ||
| 7 | 8 | ||||
| @@ -127,6 +128,14 @@ func TestRendererEnforcesExactGeneratedByteLimit(t *testing.T) { | |||||
| 127 | require.ErrorContains(t, err, "generated update is") | 128 | require.ErrorContains(t, err, "generated update is") | ||
| 128 | } | 129 | } | ||
| 129 | 130 | ||||
| 131 | + | func TestRendererAcceptsMaxIntGeneratedBytesLimit(t *testing.T) { | |||
| 132 | + | renderer, err := New(Options{Limits: Limits{MaxGeneratedBytes: math.MaxInt}}) | |||
| 133 | + | require.NoError(t, err) | |||
| 134 | + | prepared, err := renderer.Prepare(core.NewFrame(2, 1), nil, true, Cursor{}) | |||
| 135 | + | require.NoError(t, err) | |||
| 136 | + | require.NoError(t, prepared.Commit()) | |||
| 137 | + | } | |||
| 138 | + | ||||
| 130 | func TestRendererUsesIncrementalDamageWithNormalCounts(t *testing.T) { | 139 | func TestRendererUsesIncrementalDamageWithNormalCounts(t *testing.T) { | ||
| 131 | frame := core.NewFrame(2, 2) | 140 | frame := core.NewFrame(2, 2) | ||
| 132 | renderer, err := New(Options{}) | 141 | renderer, err := New(Options{}) | ||
M oracle_test.go +3 -0
| @@ -644,6 +644,9 @@ func TestDifferentialOracle(t *testing.T) { | |||
| 644 | 644 | } | |
| 645 | 645 | want := map[string][]string{} | |
| 646 | 646 | for _, line := range strings.Split(strings.TrimSpace(string(golden)), "\n") { | |
| 647 | + | if len(line) < 3 { | |
| 648 | + | t.Fatalf("malformed golden line %q (regenerate with -oracle.update on a trusted baseline)", line) | |
| 649 | + | } | |
| 647 | 650 | want[line[:2]] = append(want[line[:2]], line) | |
| 648 | 651 | } | |
| 649 | 652 | for i, digests := range got { | |
| @@ -644,6 +644,9 @@ func TestDifferentialOracle(t *testing.T) { | |||||
| 644 | } | 644 | } | ||
| 645 | want := map[string][]string{} | 645 | want := map[string][]string{} | ||
| 646 | for _, line := range strings.Split(strings.TrimSpace(string(golden)), "\n") { | 646 | for _, line := range strings.Split(strings.TrimSpace(string(golden)), "\n") { | ||
| 647 | + | if len(line) < 3 { | |||
| 648 | + | t.Fatalf("malformed golden line %q (regenerate with -oracle.update on a trusted baseline)", line) | |||
| 649 | + | } | |||
| 647 | want[line[:2]] = append(want[line[:2]], line) | 650 | want[line[:2]] = append(want[line[:2]], line) | ||
| 648 | } | 651 | } | ||
| 649 | for i, digests := range got { | 652 | for i, digests := range got { | ||