Research-0787 — libvmaf Public API Error-Path Consistency Audit¶
Date: 2026-05-29 Author: Claude Sonnet 4.6 (automated research agent) Scope: All VMAF_EXPORT functions across core/include/libvmaf/*.h and their implementations Status: Complete — findings ready for fix PR
1. Convention¶
All VMAF_EXPORT int functions uniformly document and implement:
0on success< 0(negative errno code) on failure
This convention is documented in every public header via @return 0 on success, or < 0 (a negative errno code) on error (e.g., libvmaf.h line 106, libvmaf_cuda.h line 44, libvmaf_sycl.h line 58). The convention is consistently applied across ~45 public int-returning functions. No VMAF-specific error code namespace exists; the API reuses POSIX errno values throughout.
2. Errno Usage — Consistency Findings¶
2a. -EINVAL vs -ENOMEM (generally correct)¶
NULL-argument guards return -EINVAL. Allocation failures return -ENOMEM. These are applied consistently across the CPU, CUDA, SYCL, HIP, Metal, and Vulkan backends.
2b. -EAGAIN (correct, intentional)¶
vmaf_feature_collector_get_score() returns -EAGAIN when a feature is valid but not yet written (retroactive scoring; see ADR-0154). This is documented in-source and correct.
2c. -EBUSY (fork-added, consistent but undocumented)¶
-EBUSY is returned by vmaf_sycl_preallocate_pictures(), vmaf_vulkan_preallocate_pictures(), and vmaf_use_tiny_model() / related DNN functions when called a second time after the resource is already initialised. The CUDA equivalent (vmaf_cuda_preallocate_pictures()) does not guard against double-call — it silently overwrites state. This is an inconsistency across backends.
2d. File-open error returns wrong code (BUG)¶
vmaf_write_output_with_format() (libvmaf.c lines 2948–2964) returns -EINVAL when open(2) or fdopen(3) fails. The real cause is a filesystem error (path not found, permission denied, read-only filesystem), which should surface as -EIO or -errno. The errno value is discarded and a hardcoded -EINVAL is returned, making the error indistinguishable from a bad-argument error to callers.
2e. CUDA state_init conflates driver-missing with bad-argument (BUG)¶
vmaf_cuda_state_init() returns -EINVAL for both:
- driver library load failure (
cuda_load_functionsfails — should be-ENODEVor-ENOSYS) cuInit()failure indicating no visible CUDA device (should be-ENODEV)
By contrast, vmaf_hip_state_init() and vmaf_sycl_state_init() correctly return -ENODEV for device-not-found conditions.
3. Silent-Error Findings¶
3a. vmaf_close() drops return values (DEFECT)¶
vmaf_close() calls vmaf_framesync_destroy() and vmaf_thread_pool_wait() (both int-returning) without checking or (void)-casting their return values. These violations are CERT INT15-C and NASA Power-of-10 rule (every non-void return value must be checked or explicitly discarded). The return values are discarded silently, which means thread-pool errors during shutdown are invisible to the caller.
Additionally, vmaf_picture_unref() is called inside vmaf_close() without capturing its return value.
3b. vmaf_init() all-paths return -ENOMEM (minor imprecision)¶
vmaf_init()'s error ladder always falls through to return -ENOMEM regardless of which sub-init failed. In practice, the only sub-inits that can fail are allocations, so this is currently benign. However, if vmaf_feature_extractor_list_audit() were to return -EINVAL for a registry bug, that would be silently converted to -ENOMEM.
4. void-Returning Public Functions¶
The following VMAF_EXPORT void functions exist across backends:
| Function | Notes |
|---|---|
vmaf_model_destroy() | Correct: destructor, NULL-safe |
vmaf_model_collection_destroy() | Correct: destructor, NULL-safe |
vmaf_dnn_session_close() | Correct: destructor, NULL-safe |
vmaf_mcp_close() | Correct: destructor, NULL-safe |
vmaf_sycl_state_free() | Correct: destructor, NULL-safe, double-ptr |
vmaf_sycl_dmabuf_free() | Correct: free, NULL-safe |
vmaf_sycl_profiling_disable() | Correct: fire-and-forget state reset |
vmaf_sycl_profiling_print() | Acceptable: diagnostic output; errors logged only |
vmaf_hip_state_free() | Correct: destructor, NULL-safe, double-ptr |
vmaf_metal_state_free() | Correct: destructor, NULL-safe, double-ptr |
vmaf_vulkan_state_free() | Correct: destructor, NULL-safe, double-ptr |
None of these silently mask a fatal condition. All destructors are NULL-safe.
5. Cross-Backend Inconsistencies¶
5a. state_free() signature mismatch (BUG)¶
vmaf_cuda_state_free() has a different signature from all other backend state_free functions:
| Backend | Signature |
|---|---|
| CUDA | int vmaf_cuda_state_free(VmafCudaState *cu_state) — returns int, takes single pointer |
| SYCL | void vmaf_sycl_state_free(VmafSyclState **sycl_state) — returns void, takes double pointer |
| HIP | void vmaf_hip_state_free(VmafHipState **state) — returns void, takes double pointer |
| Metal | void vmaf_metal_state_free(VmafMetalState **state) — returns void, takes double pointer |
| Vulkan | void vmaf_vulkan_state_free(VmafVulkanState **state) — returns void, takes double pointer |
CUDA's state_free (a) returns int (always 0, never an error), and (b) takes a single pointer rather than a double-pointer (so callers must manually null the pointer after the call). The three newer backends (SYCL, HIP, Metal) all use a double-pointer convention that auto-nulls the handle.
5b. preallocate_pictures double-call guard¶
SYCL and Vulkan return -EBUSY on second call. CUDA silently overwrites (see §2c).
5c. device-not-found error code¶
CUDA: returns -EINVAL (incorrect). SYCL, HIP: return -ENODEV (correct).
6. Recommendations (no implementation in this PR)¶
-
Fix
vmaf_write_output_with_format()file-open error code: captureerrnobefore logging and return-errno(or-EIOas the canonical I/O fallback). File:core/src/libvmaf.clines 2948–2964. -
Fix
vmaf_cuda_state_init()device/driver error codes: return-ENOSYSwhencuda_load_functionsfails (driver not present),-ENODEVwhencuInit()fails (driver present but no visible GPU). File:core/src/cuda/common.clines 154–183. -
Fix
vmaf_close()unchecked return values:(void)-castvmaf_framesync_destroy(),vmaf_thread_pool_wait(), andvmaf_picture_unref()calls, or propagate their errors into the return value. File:core/src/libvmaf.clines 1434–1437. -
Normalise
vmaf_cuda_state_free()to match other backends: change tovoid vmaf_cuda_state_free(VmafCudaState **cu_state)(double-pointer, void return), auto-null the handle. This is a public ABI break — requires a major ADR and version bump. Alternatively: addvmaf_cuda_state_free2(VmafCudaState **)and deprecate the old form. -
Add
-EBUSYguard tovmaf_cuda_preallocate_pictures(): match SYCL/Vulkan behaviour to prevent silent state overwrite on double-call. File:core/src/libvmaf.clines 354–383. -
vmaf_init()error propagation: replace the singlereturn -ENOMEMfallthrough withreturn errso registry-audit failures (-EINVAL) are surfaced correctly. Low priority as it is currently only reachable from a programming error, not a runtime condition.
7. Confirmed NOT Issues¶
-EBUSYfrom SYCL/Vulkan/DNN on double-call: intentional and correct.voiddestructors: all NULL-safe, none mask fatal errors.-EAGAINfromvmaf_feature_collector_get_score: intentional, documented.return 0.0in static helperresolve_feature_score_from_collector()(lines 1351/1358): this is adouble-returning internal helper, not a publicintfunction. The 0.0 is a legitimate fallback score, not a false-success errno.vmaf_init()swallowing sub-init error codes via-ENOMEMfallthrough: benign in practice (all sub-inits only fail on OOM today), but warrants the fix in rec. 6 for defensive hygiene.
Sources¶
core/include/libvmaf/libvmaf.hcore/include/libvmaf/libvmaf_cuda.hcore/include/libvmaf/libvmaf_sycl.hcore/include/libvmaf/libvmaf_hip.hcore/include/libvmaf/libvmaf_metal.hcore/include/libvmaf/libvmaf_vulkan.hcore/include/libvmaf/dnn.hcore/src/libvmaf.ccore/src/cuda/common.ccore/src/cuda/cuda_helper.cuhcore/src/hip/common.ccore/src/sycl/common.cppcore/src/picture.ccore/src/feature/feature_collector.c