diff --git a/FIDELITY.md b/FIDELITY.md index f42f7a6..da2fe67 100644 --- a/FIDELITY.md +++ b/FIDELITY.md @@ -18,6 +18,15 @@ The committed PNGs under `testdata/renders/` back every claim here. Reproduce them with the commands at the bottom. The measured-vs-Chrome numbers live in [`bench/REPORT.md`](bench/REPORT.md). +## 2026-10-09 (round 167) — the interrupted 500-page bench run (round 166's own queued work) found a second real crash: a lone CSS Grid item using `grid-row:1 / -1` with no explicit row template panicked on an empty row-offset slice (engine#266, patch) + +Round 166's fix shipped, then the still-queued 500-page bench run (launched before that fix, to measure the round-165 rotate fix) was resumed to see it through. It crashed again, on page 406: `https://www.lego.com/`, with `panic: runtime error: index out of range [0] with length 0` in `layout.grid` at `rowY[it.r0]`. Reproduced directly with `cmd/render` against the live URL, independent of the bench harness. + +- **Root cause**: `resolveAxisPlacement` (grid item row/column placement) resolves a pair of explicit grid lines (e.g. `grid-row:1 / -1`, the common "span the whole axis" idiom — Tailwind's `row-span-full` compiles to exactly this) by normalising both lines, swapping if needed so `end >= start`, then clamping only the *start* to be non-negative (a track index can't be negative) — without re-checking that the span survived the clamp. With no `grid-template-rows` at all (a grid relying purely on implicit/auto rows, nTracks=-1 so the negative end line resolves against nothing), `1 / -1` normalises to start=0, end=-2, swaps to start=-2, end=0 (a valid, non-empty `[-2,0)` span) — then clamping start alone to 0 left end at 0 too: a degenerate **zero-track span**. CSS Grid's own spec (§8.3) guarantees an item always occupies at least one track per axis; this was a real divergence from it, with a concrete crash consequence: with this as a grid's *only* item (a second, plainly-auto-placed sibling would mask the bug — its own default 1-row span raises `nRows` regardless), `nRows` never rises above 0, so `sizeRows`/`trackOffsets` return empty slices, and indexing `rowY[it.r0]` panics. +- **Fixed**: `resolveAxisPlacement`'s explicit-start-and-end case now re-checks `end <= clampedStart` *after* the clamp and widens the span back to at least 1 rather than leaving it collapsed — the same "span must stay at least 1" invariant the function's other four cases already guarantee unconditionally. +- **Test**: `TestGridRowSpanFullWithNoExplicitRowsDoesNotCrash` (`layout/grid_test.go`) — a single-item grid with no `grid-template-rows`, `grid-row:1 / -1`. Stash-verified against the unfixed code (the exact same discipline round 166 and the round-165 rotate fix both used): it panics with the identical trace without the fix, passes with it. +- **Coverage**: `layout` stayed at 100% (the fixed branch is reached unconditionally whenever this case executes at all — no separate dead branch was introduced). + ## 2026-10-07 (round 166) — a script-built DOM tree far deeper than any real page crashed every whole-document tree walk with an unrecoverable stack overflow, not a panic (security audit, patch) Continuing this session's "audit de sécurité" pass after the rotate-crash fix below: checked for ReDoS and stack-overflow risk from pathological input. Go's `regexp` package is RE2-based (confirmed: every custom pattern in this repo — `svg.go`, `css/supports.go`, `css/selector.go`, `css/container.go`, `css/parse.go` — compiles under `regexp.MustCompile`, never a backtracking engine), so ReDoS is structurally impossible here, same category of finding as round 161's XXE check. Stack-overflow risk was real. diff --git a/layout/grid.go b/layout/grid.go index e094bad..76c45c5 100644 --- a/layout/grid.go +++ b/layout/grid.go @@ -292,7 +292,23 @@ func resolveAxisPlacement(start, end css.GridLine, nTracks int) (start0, span in if e == s { e = s + 1 } - return clampNonNeg(s), e - clampNonNeg(s), true + // Clamping s to 0 can undo the e>s gap this far established: e.g. + // `grid-row:1 / -1` with no grid-template-rows (nTracks=-1, so norm + // never resolves the -1 against anything real) gives s=0, e=-2, swaps + // to s=-2, e=0 — already a non-empty [s,e) span — but THEN clamping s + // alone to 0 leaves e=0 too, a degenerate ZERO-track span. A grid + // item always occupies at least one track per axis (CSS Grid §8.3: + // an omitted/unresolvable line implies a span of 1), so re-check + // after the clamp and widen rather than collapse. Confirmed live on + // https://www.lego.com/ (round 167): a lone item using exactly this + // pattern left every row-related slice (sizeRows, trackOffsets) + // empty despite the item being non-empty, panicking grid() with + // "index out of range [0] with length 0" at rowY[it.r0]. + cs := clampNonNeg(s) + if e <= cs { + e = cs + 1 + } + return cs, e - cs, true case !start.Auto && !start.Span && end.Span: s := clampNonNeg(norm(start.N)) return s, maxInt(end.N, 1), true diff --git a/layout/grid_test.go b/layout/grid_test.go index 45a6858..6b1d4db 100644 --- a/layout/grid_test.go +++ b/layout/grid_test.go @@ -172,6 +172,37 @@ func TestGridRowSpan(t *testing.T) { assertF(t, "rspan.C.Y", byText["C"].Y, 30) // second row } +func TestGridRowSpanFullWithNoExplicitRowsDoesNotCrash(t *testing.T) { + // TestGridRowSpanFullNegativeLineReservesOccupancy (below) covers + // `grid-row:1 / -1` with an explicit grid-template-rows, where the + // negative end line resolves against that explicit track count. With NO + // grid-template-rows at all (a grid relying purely on implicit/auto + // rows — common for a single-row "sidebar spans everything" layout), + // resolveAxisPlacement's nTracks argument is -1 (unresolved), and its + // norm() helper then does a bare `n-1` on the negative end line with + // nothing to resolve against: start=1 -> norm=0, end=-1 -> norm=-2, the + // e=1 span of its own — a second item auto-placed at + // row 0 span 1 would mask the bug by raising nRows to 1 regardless), + // nRows never rises above nRowsExplicit(0), so sizeRows/trackOffsets + // return empty slices and grid()'s `rowY[it.r0]` panics with "index out + // of range [0] with length 0". Confirmed live on https://www.lego.com/ + // during the 500-page bench corpus (round 167) — a real, reachable + // crash, not a contrived one; reproduced with `cmd/render` against the + // live URL, independent of the bench harness. + src := `
` + + `
A
` + g := findBox(layoutHTML(t, src, 400), "div") + if len(g.Children) != 1 { + t.Fatalf("want 1 grid item, got %d", len(g.Children)) + } + if g.Children[0].H <= 0 { + t.Errorf("item should have a non-zero height, got %v", g.Children[0].H) + } +} + func TestGridRowSpanFullNegativeLineReservesOccupancy(t *testing.T) { // `grid-row: 1 / -1` (Tailwind's row-span-full) in a 3-row explicit grid // must span all 3 rows and reserve column 0 in every row, exactly like a