Skip to content

feature/adm: reject frames below 17 pixels in either dimension at init - #1642

Open
lusoris wants to merge 2 commits into
Netflix:masterfrom
VMAFx:fix/adm-tiny-frame-crash
Open

lusoris wants to merge 2 commits into
Netflix:masterfrom
VMAFx:fix/adm-tiny-frame-crash

Conversation

@lusoris

@lusoris lusoris commented Oct 1, 2026 •

Copy link
Copy Markdown

Integer ADM crashes on any frame whose width or height is 16 or below. This makes init() refuse those frames with -EINVAL and a message that names the limit. Fixes #1607.

Why refuse instead of compute

#1607 asked whether these sizes should work or be refused, and nobody has answered. The history points at refusing: #1035 reported this crash in 2022, #1046 fixed it by rejecting w <= 32 || h <= 32 in init() ("ADM probably doesn't make sense as a metric at these very small patch sizes"), and 966be8d (#1485) dropped that check.

float_adm is the reference definition, and it does not define a score for these frames either. Same synthetic 4:2:0 content (3 frames, pseudo-random luma/chroma with a distortion of up to +-12), float_adm at frame index 2:

size float_adm
1x1 to 8x8 ASan heap-buffer-overflow READ of size 4 in adm_dwt2_s (adm_tools.c:1091, called from compute_adm, adm.c:197; at 8x8 it is 32 bytes before a 2560-byte region); also 64x1, 64x2, 64x8, 1x64, 2x64, 8x64
9x9 to 16x16 no sanitizer report; VMAF_feature_adm2_score 1.383, 1.297, 1.397, 0.995, 1.402, 1.327, 1.224, 1.424 (9 to 16), adm_scale3 up to 3.25
17x17 0.973
32x32 0.984
64x64 0.98597 (integer ADM on the same frames: 0.98597)

ADM is a ratio of retained to original detail, so a score of 1.4 (and 3.1 for scale 3 at 16x16) is not a measurement. Making the integer path return numbers at these sizes would give it values its own reference does not have, so it refuses them.

Cause

Two defects behind the crash, both from #1607 and re-checked on master 8e7a1ac4:

  1. adm_cm() computes (uint32_t)ceil(log2(w) - 4) for shift_xhcub and shift_xvcub (- 3 for shift_xdcub). For a scale 0 band of 8 samples or fewer, that is a frame dimension of 16 or less, the double is negative and converting it to uint32_t is undefined; every >> shift_xhcub after it shifts by 4294967295. The same expression is in adm_avx2.c (adm_cm_avx2), adm_avx512.c (adm_cm_avx512) and in the CUDA host code (integer_adm_cuda.c).
  2. dwt2_src_indices_filt() loops for (i = 1; i < h_half - 2; ++i) with an unsigned h_half. A 16-pixel dimension reaches scale 3 with a 2-sample input (16 to 8 to 4 to 2), so h_half == 1, the bound wraps to 4294967295 and the loop writes past the index table. Same for w_half.

Where the boundary is: a dimension d reaches scale 3 as a DWT input of ceil(d / 8) samples and the transform needs 3, which is d >= 17. That is also exactly where ceil(log2(band) - 4) stops being negative.

Reproducer

Master 9e48141b, release build, x86-64, 16x16 4:2:0 clips cut from the src01 pair (3 frames):

head -c 1152 python/test/resource/yuv/src01_hrc00_576x324.yuv > ref_16x16.yuv
head -c 1152 python/test/resource/yuv/src01_hrc01_576x324.yuv > dis_16x16.yuv
build/tools/vmaf -r ref_16x16.yuv -d dis_16x16.yuv -w 16 -h 16 -p 420 -b 8 \
  --feature adm --no_prediction -q -o /dev/null
size master this branch
16x16 exit 139 libvmaf ERROR adm: invalid size (16x16), width and height must be at least 17, exit 234
64x16 exit 139 same message with 64x16, exit 234
16x64 exit 139 same message with 16x64, exit 234
24x24 exit 0 exit 0

With --model version=vmaf_v0.6.1 instead of --feature adm, this branch refuses the 16x16 clip with the same message and exit 234.

Fix

init() returns -EINVAL and logs the size and the limit when w or h is below ADM_MIN_DIM (17, defined in integer_adm.h with the derivation). Frames of 17 and above take the same path as before. A second commit applies the same check at the top of init_fex_cuda(), because adm_cuda carries the same host expression and is picked for the same frames when a CUDA context exists; drop that commit if you would rather keep this CPU-only.

The process exit status for the refused run changes from 139 (SIGSEGV) to 234 (-EINVAL as an unsigned byte).

What this does not touch

Tests

New test_adm_minimum_dimension in libvmaf/test/test_feature_extractor.c: init() fails with -EINVAL for 1x1, 8x8, 16x16, 64x16 and 16x64 and succeeds for 17x17, 64x17, 17x64 and 64x64. With master's integer_adm.c and the new test, the case fails ("adm init should reject a frame below 17 pixels in either dimension") in a release build, no sanitizer needed; with this change test_feature_extractor passes 5/5.

Validation

x86-64 Linux (AVX-512 host), GCC 16.2.1, on master 9e48141b. The sanitizer builds use -Db_sanitize=address,undefined -Db_lto=false and add -fsanitize=float-cast-overflow, which GCC's undefined group does not include.

Rebased on master 9e48141b (2026-10-02). The size sweep and the CUDA run below were taken on 8e7a1ac4 and not repeated (integer_adm.c, adm_avx2.c, adm_avx512.c and integer_adm_cuda.c are unchanged between the two, and integer_adm.c differs only by one NEON dispatch line in init(), after the lines this change touches); the reproducer table, the meson test counts and the Netflix pair comparison were re-run on 9e48141b. On the CUDA build, meson test is 24 of 25 on master and here (test_cuda_pic_preallocation fails on both), and the CLI output with --gpumask 0 on the three Netflix pairs is identical to master.

The workflow run on this PR needs a maintainer's approval.

@lusoris
lusoris force-pushed the fix/adm-tiny-frame-crash branch from dfc7a12 to 11e7e7a Compare October 2, 2026 05:23
Lusoris and others added 2 commits October 2, 2026 20:32
integer ADM crashes on any frame whose width or height is 16 or below.
Two defects are behind it. adm_cm() converts ceil(log2(w) - 4), which is
negative for a scale 0 band of 8 samples or fewer, to uint32_t, so every
`>> shift_xhcub` shifts by 4294967295. And dwt2_src_indices_filt() loops
to `h_half - 2` on an unsigned h_half, which wraps when scale 3 gets an
input of 2 samples, the case for a 16 pixel dimension.

float_adm does not define a score for these frames either: it reads out
of bounds up to 8x8 and returns adm2 scores above 1 from 9x9 to 16x16.
So refuse them in init() with -EINVAL and a message, instead of making
the integer path compute a number the float reference does not have.
The limit is 17 because scale 3 needs a DWT input of 3 samples. Frames
of 17 and above are unchanged.

Add a test_feature_extractor case: init fails for 1x1, 8x8, 16x16,
64x16 and 16x64 and succeeds for 17x17, 64x17, 17x64 and 64x64.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
adm_cuda computes the same ceil(log2(w) - 4) shift counts on the host
(integer_adm_cuda.c) and is picked for the same frames when a CUDA
context is present, so it must refuse what the CPU extractor refuses.
Share ADM_MIN_DIM through integer_adm.h and check it at the top of
init_fex_cuda(), before any CUDA object is created.

Built with CUDA 13.4 and run on an RTX 4090: 16x16, 64x16 fail with
"adm_cuda: invalid size" and exit 234, 24x24 scores as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@lusoris
lusoris force-pushed the fix/adm-tiny-frame-crash branch from 11e7e7a to bee03b9 Compare October 2, 2026 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

integer ADM crashes for any frame dimension of 16 or below

1 participant