ADR-1480: speed_temporal sizes its frame buffers for the prescaled height; upstream sizes them for the source height and reads past them when speed_prescale is above 1¶
- Status: Accepted
- Date: 2026-10-02
- Deciders: lusoris
- Tags:
upstream-divergence,speed,memory-safety,correctness,rc3,fork-local
Context¶
The reference for code the fork inherited from Netflix/vmaf is Netflix's source; a difference needs an ADR (see References). The upstream parity audit of 2026-10-02 found this difference without one.
Upstream, libvmaf/src/feature/speed.c at Netflix 9e48141b: speed_temporal allocates its four frame buffers as float_stride * h, the source height (:1578 to :1582). filter_and_downscale() copies stride_px * dim.alloc_height floats out of such a buffer (:957, :968), and alloc_height is the larger of the source and the prescaled height (:1049). With speed_prescale above 1 the prescaled frame is taller than the source, so the copy reads past the buffer and the prescaled frame is written past it. speed_chroma in the same file allocates with alloc_height (:1352, :1356) and is not affected.
The fork's core/src/feature/speed.c allocates the four buffers as float_stride * alloc_height (PR #1643, c1a2b1914, 2026-09-30). The pull request called it a bug fix matching the fork's own upstream pull request and wrote no ADR.
Decision¶
The fork keeps speed_temporal's frame buffers at alloc_height rows. With speed_prescale above 1 its scores differ from upstream's by design: upstream computes them from memory outside its buffers. At speed_prescale of 1 or less alloc_height equals the source height and nothing differs.
Alternatives considered¶
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
| Restore upstream's allocation | Same code as upstream | Heap overrun on every frame at speed_prescale above 1; the value depends on what lies behind the buffer, and upstream's own scalar and SIMD runs disagree there | A memory-safety defect is not a reference |
Refuse speed_prescale above 1 | No value that differs from upstream | Removes an option value upstream documents; the correct allocation is known | The fix is one multiplication |
Consequences¶
- Measured size (audit of 2026-10-02: Netflix
cea2b4d8, whosespeed.cis9e48141b's, against the fork with its unintended differences reverted; GCC 16.2.1, x86-64, C API at%.17g;speed_prescale=2.0:speed_prescale_method=lanczos4): scalar dispatch, the Netflix 576x324 pair: 1 of 48 frames identical, at most 195 apart (frame 36: upstream 208.31849670410156, fork 13.179099082946777); 1920x1080 checkerboard 131; the 10-bit pair 117; the noise pair 12.3. Upstream ends in a segmentation fault on the 160x90 pair (both dispatches) and on the noise pair (default dispatch); the fork returns finite values on all five. Between its own scalar and default dispatch, upstream'sspeed_temporalagrees on 1096 of 1122 values of the probe set (at most 131 apart); the fork's dispatches agree on all. - Upstream status: Netflix/vmaf#1627 (open, sent by the fork) makes the same allocation change; Netflix/vmaf#1626 is the report.
- Ends when upstream merges #1627. The values then agree, and the allowlist entry of the upstream parity guard for this deviation goes stale and is removed.
- Neutral: PR #1643 also made
speed_prescale_resamples()resample when rounding changes a plane extent although the factor is within 0.001 of 1; no option value of the audit reaches that case. Itsspeed_chromapart (chroma extents rounded up on odd sizes) belongs to ADR-1483. - Neutral:
core/test/test_speed_frame_buffers.cguards the allocation.
References¶
- Fork PR #1643 (
c1a2b1914);docs/state.mdrowT-SPEED-TEMPORAL-PRESCALE-UP-OVERFLOW-2026-09-30. - Upstream:
libvmaf/src/feature/speed.c:957,:968,:1049,:1578at Netflix9e48141b; Netflix/vmaf#1627, Netflix/vmaf#1626. - Source:
req(popup answer, 2026-10-02): "Netflix's source, deviations only by ADR".