Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions core/src/feature/float_vif.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "feature_collector.h"
#include "feature_extractor.h"
#include "feature_name.h"
#include "log.h"
#include "mem.h"

#include "vif.h"
Expand Down Expand Up @@ -192,13 +193,36 @@

VifState *s = fex->priv;

/*
* The scale-0 Gaussian uses a 17-tap separable filter. The reflect-101
* mirror-padding formula `ii = 2*h - ii - 2` requires the dimension to be
* >= 9 to stay in-bounds for every filter position (half-width = 8,
* worst-case index = dim + 7, mirrored = dim - 9 >= 0).
* Guard on the raw input dimensions first (before any string-option access)
* to provide a fast, unconditional early exit. A second guard after
* computing scaled_w / scaled_h covers the case where a prescale < 1.0
* shrinks a larger frame below the minimum.
*/
if (w < 9 || h < 9) {
vmaf_log(VMAF_LOG_LEVEL_ERROR,
"float_vif requires width >= 9 and height >= 9 (got %ux%u)\n", w, h);
return -EINVAL;
}

enum vif_scaling_method scaling_method;
if (vif_get_scaling_method(s->vif_prescale_method, &scaling_method)) {
return -EINVAL;
}

s->scaled_w = (int)(w * s->vif_prescale + 0.5);
s->scaled_h = (int)(h * s->vif_prescale + 0.5);

if (s->scaled_w < 9 || s->scaled_h < 9) {
vmaf_log(VMAF_LOG_LEVEL_ERROR,
"float_vif requires scaled width >= 9 and height >= 9 (got %dx%d)\n", s->scaled_w,
s->scaled_h);
return -EINVAL;
}
s->float_stride = ALIGN_CEIL(w * sizeof(float));
s->scaled_float_stride = ALIGN_CEIL(s->scaled_w * sizeof(float));
s->ref = aligned_malloc(s->float_stride * h, 32);
Expand Down
6 changes: 6 additions & 0 deletions core/src/feature/integer_adm.c
Original file line number Diff line number Diff line change
Expand Up @@ -3344,6 +3344,12 @@ static int init(VmafFeatureExtractor *fex, enum VmafPixelFormat pix_fmt, unsigne
(void)pix_fmt;
(void)bpc;

if (w < 17u || h < 17u) {
vmaf_log(VMAF_LOG_LEVEL_ERROR,
"integer_adm requires width >= 17 and height >= 17 (got %ux%u)\n", w, h);
return -EINVAL;
}

s->dwt2_8 = adm_dwt2_8;
s->dwt2_16 = adm_dwt2_16;
s->adm_decouple = adm_decouple;
Expand Down
27 changes: 26 additions & 1 deletion core/src/libvmaf.c
Original file line number Diff line number Diff line change
Expand Up @@ -1649,6 +1649,14 @@ static void threaded_extract_batch_func(void *e, void **thread_data)
if (shared_fex->flags & VMAF_FEATURE_EXTRACTOR_CUDA)
continue;

/* SYCL extractors are dispatched via the SYCL command-graph path in
* read_pictures_dispatch_one(); the CPU thread pool must skip them
* symmetrically with CUDA to prevent double-dispatch. Without this
* guard, every SYCL extractor runs once via the SYCL path and once
* via this pool, corrupting the feature-collector state. */
if (shared_fex->flags & VMAF_FEATURE_EXTRACTOR_SYCL)
continue;

if (shared_fex->flags & VMAF_FEATURE_EXTRACTOR_TEMPORAL)
continue;

Expand Down Expand Up @@ -1980,12 +1988,24 @@ static int flush_context_cuda(VmafContext *vmaf)
RegisteredFeatureExtractors rfe = vmaf->registered_feature_extractors;
for (unsigned i = 0; i < rfe.cnt; i++) {
if (rfe.fex_ctx[i]->fex->flags & VMAF_FEATURE_EXTRACTOR_CUDA) {
// Collect any pending double-buffered CUDA work
/* Collect any pending double-buffered CUDA work. The thread pool
* path (flush_context_threaded) does not drain gpu_pending for
* CUDA extractors, so this collect must run regardless. */
if (rfe.fex_ctx[i]->gpu_pending) {
err |= vmaf_feature_extractor_context_collect(
rfe.fex_ctx[i], rfe.fex_ctx[i]->gpu_pending_index, vmaf->feature_collector);
rfe.fex_ctx[i]->gpu_pending = false;
}
/* flush_context_threaded already called
* vmaf_feature_extractor_context_flush() on every TEMPORAL
* extractor (including those also flagged CUDA) via its first
* loop (lines ~1896-1899). Calling flush a second time on the
* same feature_collector index produces a duplicate-write
* -EINVAL and leaves cuCtxSynchronize in an error state.
* Skip the flush here for temporal-CUDA extractors when the
* thread pool was active. */
if (vmaf->thread_pool && (rfe.fex_ctx[i]->fex->flags & VMAF_FEATURE_EXTRACTOR_TEMPORAL))
continue;
err |= vmaf_feature_extractor_context_flush(rfe.fex_ctx[i], vmaf->feature_collector);
}
}
Expand Down Expand Up @@ -2292,6 +2312,11 @@ static bool read_pictures_should_skip(VmafContext *vmaf, VmafFeatureExtractorCon
}
}

/* CPU extractors with a thread pool go to the threaded batch path.
* CUDA + SYCL extractors run serially via their respective dispatch loops.
* Skipping them in the threaded batch and skipping them ALSO from the serial
* loop here would leak — be careful to keep these in sync with the changes
* to threaded_extract_batch_func. */
if (!(fex_ctx->fex->flags & VMAF_FEATURE_EXTRACTOR_CUDA) &&
!(fex_ctx->fex->flags & VMAF_FEATURE_EXTRACTOR_SYCL) && vmaf->thread_pool) {
if (!(fex_ctx->fex->flags & VMAF_FEATURE_EXTRACTOR_TEMPORAL))
Expand Down
44 changes: 44 additions & 0 deletions core/test/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -1618,6 +1618,27 @@ test_float_ms_ssim_min_dim = executable('test_float_ms_ssim_min_dim',
)
test('test_float_ms_ssim_min_dim', test_float_ms_ssim_min_dim, suite : ['fast'])

# integer_adm init() must reject min(w,h) <= 16 with -EINVAL (SIGSEGV guard):
# the 4-level DWT2 pyramid requires at least 17 pixels in each dimension.
test_integer_adm_min_dim = executable('test_integer_adm_min_dim',
['test.c', 'test_integer_adm_min_dim.c', '../src/mem.c', '../src/picture.c',
'../src/ref.c', '../src/dict.c', '../src/predict.c',
'../src/metadata_handler.cpp', '../src/thread_locale.c', '../src/gpu_picture_pool.cpp', dnn_sources],
include_directories : [libvmaf_inc, test_inc, include_directories('../src/'), dnn_inc],
c_args : vmaf_cflags_common + dnn_defines,
dependencies : [math_lib, stdatomic_dependency, pthread_dependency, thread_lib,
gpu_all_deps, dnn_deps],
objects : [
common_cuda_objects,
platform_specific_cpu_objects,
libvmaf_feature_static_lib.extract_all_objects(recursive: true),
log_cpp23_test_objects,
libvmaf_cpu_static_lib.extract_all_objects(recursive: true),
libsvm_static_lib.extract_all_objects(recursive: true),
] + wave8_opt_only_objects
)
test('test_integer_adm_min_dim', test_integer_adm_min_dim, suite : ['fast'])

# Research-0094 regression: motion init() must reject frames < 3x3 with -EINVAL
# instead of reading out-of-bounds via the reflect-101 mirror-padding formula.
test_motion_min_dim = executable('test_motion_min_dim',
Expand All @@ -1639,6 +1660,29 @@ test_motion_min_dim = executable('test_motion_min_dim',
)
test('test_motion_min_dim', test_motion_min_dim, suite : ['fast'])

# float_vif min-dimension guard — regression test for double-free / OOB-read
# when min(w,h) <= 6 triggers the 17-tap Gaussian filter at scale 0.
# Fixed by inserting a w < 7 || h < 7 guard in float_vif init() before any
# scratch-buffer allocation (PR fix/float-vif-min-dimension-guard).
test_float_vif_min_dim = executable('test_float_vif_min_dim',
['test.c', 'test_float_vif_min_dim.c', '../src/mem.c', '../src/picture.c',
'../src/ref.c', '../src/dict.c', '../src/predict.c',
'../src/metadata_handler.cpp', '../src/thread_locale.c', '../src/gpu_picture_pool.cpp', dnn_sources],
include_directories : [libvmaf_inc, test_inc, include_directories('../src/'), dnn_inc],
c_args : vmaf_cflags_common + dnn_defines,
dependencies : [math_lib, stdatomic_dependency, pthread_dependency, thread_lib,
gpu_all_deps, dnn_deps],
objects : [
common_cuda_objects,
platform_specific_cpu_objects,
libvmaf_feature_static_lib.extract_all_objects(recursive: true),
log_cpp23_test_objects,
libvmaf_cpu_static_lib.extract_all_objects(recursive: true),
libsvm_static_lib.extract_all_objects(recursive: true),
] + wave8_opt_only_objects
)
test('test_float_vif_min_dim', test_float_vif_min_dim, suite : ['fast'])

# GPU dispatch-runtime coverage (host-only). Targets the shared
# `gpu_dispatch_env.c` + `gpu_dispatch_parse.h` helpers and the
# host-buildable subset of the per-backend dispatch_strategy TUs
Expand Down
140 changes: 140 additions & 0 deletions core/test/test_float_vif_min_dim.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* Copyright 2026 Lusoris
* SPDX-License-Identifier: BSD-3-Clause-Plus-Patent
*
* Regression test — float_vif must reject frames smaller than 7x7 at
* init() time with -EINVAL instead of proceeding into the scale-0
* Gaussian filter path, which uses a 17-tap kernel and requires at
* least 7 pixels in each dimension to avoid a double-free / out-of-
* bounds read in the filter scratch buffer.
*
* Bug: float_vif init() allocated VIF scratch buffers unconditionally.
* When min(scaled_w, scaled_h) < 9 the 17-tap Gaussian filter at scale 0
* walks its reflect-101 mirror index out of the allocated region
* (half-width = 8, worst-case mirrored index = h - 9 which underflows for
* h < 9), triggering UB (ASan heap-buffer-overflow or double-free on
* close()).
*
* Fix: float_vif init() now checks scaled_w < 9 || scaled_h < 9 and
* returns -EINVAL with a human-readable log message before any allocation.
*
* This file exercises the CPU path only (GPU paths require a live GPU
* device and are covered by per-backend smoke tests).
*/

#include <errno.h>
#include <stdlib.h>

#include "libvmaf/picture.h"

#include "opt.h"
#include "test.h"

#include "feature/feature_extractor.h"

/* Allocate priv, apply option defaults (so string fields like
* vif_prescale_method are not NULL), call init(), then call close() and
* free priv. Returns the init() return code.
*
* float_vif has VMAF_OPT_TYPE_STRING options (vif_prescale_method) that are
* dereferenced inside init() before the dimension guard fires. Applying
* defaults first via vmaf_option_set(opt, priv, NULL) avoids a strcmp(NULL,…)
* crash for the acceptance tests. The rejection tests (w/h < 9) return
* -EINVAL before reaching the string option access, so they are unaffected. */
static int invoke_init(VmafFeatureExtractor *fex, unsigned w, unsigned h)
{
void *priv = calloc(1, fex->priv_size);
if (!priv)
return -1;
fex->priv = priv;

if (fex->options) {
for (unsigned i = 0; fex->options[i].name; i++) {
int err = vmaf_option_set(&fex->options[i], priv, NULL);
if (err) {
free(priv);
fex->priv = NULL;
return err;
}
}
}

int rc = fex->init(fex, VMAF_PIX_FMT_YUV420P, 8u, w, h);
if (fex->close)
(void)fex->close(fex);
free(priv);
fex->priv = NULL;
return rc;
}

/* ------------------------------------------------------------------ */
/* float_vif — reject frames below the 7x7 floor */
/* ------------------------------------------------------------------ */

static char *test_float_vif_rejects_1x1(void)
{
VmafFeatureExtractor *fex = vmaf_get_feature_extractor_by_name("float_vif");
mu_assert("float_vif extractor missing", fex != NULL);
int rc = invoke_init(fex, 1u, 1u);
mu_assert("float_vif: init(1x1) must return -EINVAL", rc == -EINVAL);
return NULL;
}

static char *test_float_vif_rejects_8x8(void)
{
VmafFeatureExtractor *fex = vmaf_get_feature_extractor_by_name("float_vif");
mu_assert("float_vif extractor missing", fex != NULL);
/* 17-tap filter half-width=8: mirror formula 2*h-ii-2 underflows for h<9 */
int rc = invoke_init(fex, 8u, 8u);
mu_assert("float_vif: init(8x8) must return -EINVAL (below 9-pixel floor)", rc == -EINVAL);
return NULL;
}

static char *test_float_vif_rejects_Nx8(void)
{
VmafFeatureExtractor *fex = vmaf_get_feature_extractor_by_name("float_vif");
mu_assert("float_vif extractor missing", fex != NULL);
/* width above floor but height below */
int rc = invoke_init(fex, 64u, 8u);
mu_assert("float_vif: init(64x8) must return -EINVAL (height < 9)", rc == -EINVAL);
return NULL;
}

static char *test_float_vif_rejects_8xN(void)
{
VmafFeatureExtractor *fex = vmaf_get_feature_extractor_by_name("float_vif");
mu_assert("float_vif extractor missing", fex != NULL);
/* height above floor but width below */
int rc = invoke_init(fex, 8u, 64u);
mu_assert("float_vif: init(8x64) must return -EINVAL (width < 9)", rc == -EINVAL);
return NULL;
}

static char *test_float_vif_accepts_9x9(void)
{
VmafFeatureExtractor *fex = vmaf_get_feature_extractor_by_name("float_vif");
mu_assert("float_vif extractor missing", fex != NULL);
int rc = invoke_init(fex, 9u, 9u);
mu_assert("float_vif: init(9x9) must succeed (exact minimum)", rc == 0);
return NULL;
}

static char *test_float_vif_accepts_576x324(void)
{
VmafFeatureExtractor *fex = vmaf_get_feature_extractor_by_name("float_vif");
mu_assert("float_vif extractor missing", fex != NULL);
int rc = invoke_init(fex, 576u, 324u);
mu_assert("float_vif: init(576x324) must succeed (Netflix golden resolution)", rc == 0);
return NULL;
}

char *run_tests(void)
{
mu_run_test(test_float_vif_rejects_1x1);
mu_run_test(test_float_vif_rejects_8x8);
mu_run_test(test_float_vif_rejects_Nx8);
mu_run_test(test_float_vif_rejects_8xN);
mu_run_test(test_float_vif_accepts_9x9);
mu_run_test(test_float_vif_accepts_576x324);
return NULL;
}
85 changes: 85 additions & 0 deletions core/test/test_integer_adm_min_dim.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Copyright 2026 Lusoris
* SPDX-License-Identifier: BSD-3-Clause-Plus-Patent
*
* integer_adm init() must reject inputs where min(w,h) <= 16 with -EINVAL
* instead of SIGSEGV-ing mid-run. The 4-level DWT2 pyramid requires at
* least 17 pixels in each dimension so that the coarsest level still has
* a non-empty band; frames smaller than 17x17 walk off the end of the
* allocated scratch buffers on the first decomposition step.
*/

#include <stdlib.h>

#include "test.h"

#include "feature/feature_extractor.h"

static char *test_integer_adm_is_registered(void)
{
VmafFeatureExtractor *fex = vmaf_get_feature_extractor_by_name("adm");
mu_assert("integer_adm extractor missing", fex != NULL);
mu_assert("integer_adm.init must be set", fex->init != NULL);
mu_assert("integer_adm.close must be set", fex->close != NULL);
return NULL;
}

/* Helper: call init with the given dimensions and return the result,
* cleanly freeing the priv buffer on the failure path. */
static int invoke_init(VmafFeatureExtractor *fex, unsigned w, unsigned h)
{
void *priv = calloc(1, fex->priv_size);
if (!priv)
return -1;
fex->priv = priv;
int rc = fex->init(fex, VMAF_PIX_FMT_YUV420P, 8u, w, h);
/* close() tolerates partial state (init may have returned early). */
(void)fex->close(fex);
free(priv);
fex->priv = NULL;
return rc;
}

static char *test_integer_adm_init_rejects_below_min_dim(void)
{
VmafFeatureExtractor *fex = vmaf_get_feature_extractor_by_name("adm");
mu_assert("integer_adm extractor missing", fex != NULL);

/* Both dimensions below the floor. */
mu_assert("init must reject 8x8 (< 17x17)", invoke_init(fex, 8u, 8u) < 0);

/* Width at floor, height below. */
mu_assert("init must reject 17x16 (h just below)", invoke_init(fex, 17u, 16u) < 0);

/* Height at floor, width below. */
mu_assert("init must reject 16x17 (w just below)", invoke_init(fex, 16u, 17u) < 0);

/* Exactly at the excluded boundary (16 is the last rejected value). */
mu_assert("init must reject 16x16", invoke_init(fex, 16u, 16u) < 0);

return NULL;
}

static char *test_integer_adm_init_accepts_min_dim(void)
{
VmafFeatureExtractor *fex = vmaf_get_feature_extractor_by_name("adm");
mu_assert("integer_adm extractor missing", fex != NULL);

/* Exact boundary — must succeed. */
int rc = invoke_init(fex, 17u, 17u);
mu_assert("init must accept 17x17 (exact minimum)", rc == 0);

/* Standard Netflix test resolution well above the floor. */
rc = invoke_init(fex, 576u, 324u);
mu_assert("init must accept 576x324 (well above minimum)", rc == 0);

return NULL;
}

char *run_tests(void)
{
mu_run_test(test_integer_adm_is_registered);
mu_run_test(test_integer_adm_init_rejects_below_min_dim);
mu_run_test(test_integer_adm_init_accepts_min_dim);
return NULL;
}
Loading
Loading