ADR-1007: Fix C string/numeric UB cluster — NULL strcmp, size_t underflow, signed-shift overflow, snprintf truncation¶
- Status: Accepted
- Date: 2026-06-04
- Deciders: Lusoris
- Tags:
core,security,c,ub
Context¶
Static analysis and audit (r3/r4 review round) identified four related undefined-behaviour and correctness defects in the C core, all in paths that are reachable from normal user input:
-
feature_name.c:108—strcmp(opt->default_val.s, *((char**)data))is called unconditionally. AnyVMAF_OPT_TYPE_STRINGoption registered with a NULL default (default_val.s = NULL) or invoked before the option value is set producesstrcmp(NULL, ...), which is UB and crashes on most implementations. The same NULL pointer is later passed tosnprintf(..., "%s", ...)at line 152. -
model.c:247—strlen(model->name) - 5issize_tarithmetic; when the model name is shorter than five characters the subtraction wraps toSIZE_MAX - 4, feeding a near-SIZE_MAXvalue intocalloc. On most platforms this OOM-fails silently; on a 64-bit system with overcommit it produces a heap overwrite via the subsequentstrncpy. -
integer_adm.c:1114,1119,1124—1 << (14 + kh_shift)wherekh_shiftcan reach 17 makes the shift operand 31 on a signedintliteral. C signed left-shift into the sign bit is UB; compilers with-fstrict-overfloware permitted to elide the entire expression or produce wrong values. Mirrored at line 2099 for the width-direction. -
integer_adm.c:1563-1566—uint32_t add_shift_cub = (uint32_t)pow(2, (shift_cub - 1))whereshift_cubis computed fromceil(log2(right-left)). Whenright-left == 1,log2(1) = 0,shift_cub = 0, andshift_cub - 1wraps toUINT32_MAX.pow(2, UINT32_MAX)is +Inf; casting +Inf touint32_tis UB. Second instance at line 2104. -
cambi.c:735—(void)snprintf(path, sizeof(path), ...)discards the return value. A longheatmaps_pathoption value silently truncates the constructed file path; the subsequentopen()creates a file at the wrong location with no diagnostic.
None of these are reachable via the standard Netflix model paths, but they are reachable via the public API (custom models, custom options, unusual frame dimensions).
Decision¶
Fix all five defects in-place with the minimal-diff approach:
feature_name.coption_is_default: guard both string operands beforestrcmp;vmaf_feature_name_from_options: null-check the string pointer beforesnprintf.model.c: addif (strlen(model->name) < 5) goto fail_mc;before the subtraction.integer_adm.cshift literals: change1to1uand cast result toint64_tto keep the arithmetic unsigned before widening.integer_adm.cpow(2, shift-1): guard with(shift > 0u) ? ... : 0u.cambi.c: capturesnprintfreturn value and return-ENAMETOOLONGon truncation.
Alternatives considered¶
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
| Suppress with NOLINT | Zero diff | Masks real crash/UB | No: ADR-0278 requires inline justification for load-bearing invariants only |
| Add assertions in tests only | Catches regression | Does not fix the UB | Insufficient: UB may be exploited before the test fires |
| Minimal in-place guard | Tiny diff, no logic change | None | Chosen |
Consequences¶
- Positive: Eliminates five UB sources reachable via public API;
UBSanandASanwill no longer fire on these paths. - Neutral: No change to any Netflix golden-data assertion values. The guarded paths are only active for non-default / unusual option values.
- Negative: None.
References¶
- r3/r4 audit findings:
[r3-string-handling]and[r3-numeric-ub]clusters - ADR-0141 (touched-file cleanup rule)
- ADR-0278 (NOLINT citation closeout)