Skip to content

fix(core): sanitizer-pass cleanup — CAMBI option type-mismatch + ADM signed-shift UB - #352

Merged
lusoris merged 1 commit into
masterfrom
fix/sanitizer-pass-cleanup
May 31, 2026
Merged

lusoris merged 1 commit into
masterfrom
fix/sanitizer-pass-cleanup

Conversation

@lusoris

@lusoris lusoris commented May 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Local -Db_sanitize=address,undefined audit on master tip bbcaa8d127
surfaced two real undefined-behaviour findings that the per-PR CI
sanitizer combination is not currently catching. Both are bit-exact
fixes — no numerical-correctness delta.

  1. CAMBI option-parser misaligned uint16_t write. Options table
    declared window_size and max_log_contrast as VMAF_OPT_TYPE_INT
    but the underlying CambiState fields were uint16_t. The
    parser's *(int *)data = value (a) silently clobbered adjacent
    src_window_size when writing window_size and (b) hit a
    UBSan-flagged misaligned 4-byte store on max_log_contrast
    (2-byte-aligned slot). Mirror read-side misalignment in
    vmaf_feature_name_from_options (core/src/feature/feature_name.c:104)
    per frame.

  2. AVX2 / AVX-512 ADM DWT2 filter packing — left-shift of negative
    signed int.
    (uint32_t)(filter[k] << 16) casts the result, not
    the operand — the inner shift runs on a signed int. UB on every
    HBD ADM frame; UBSan reports left shift of negative value -4240.

Type

  • fix — bug fix
  • simd — backend-specific (AVX2 / AVX-512 ADM)

Checklist

  • Conventional Commits.
  • pre-commit run --files is green locally on every touched file
    (clang-format, secrets, ADR-0332, ADR-0105, ADR-0386 checks all
    pass; semgrep clean on the C diff).
  • Unit tests pass: meson test -C build-asan — 63/63 OK with ASan
    + UBSan + leak detection enabled.
  • Touched SIMD code paths (AVX2 + AVX-512 ADM): bit-exact fix
    (cast move inside parentheses); /cross-backend-diff style
    worst-ULP check unchanged. test_integer_adm_simd still
    passes — scalar / AVX2 / AVX-512 ULP 0.
  • All SIMD/GPU twins of the touched extractor: the AVX2 + AVX-512
    twins both fixed in this PR (only x86 twins existed for ADM
    DWT2 filter packing).
  • No new files require licence headers (C edits are in existing
    files; the new docs/changelog files are markdown).
  • Not a breaking change.

Bug-status hygiene (ADR-0165)

  • docs/state.md updated — new row
    T-SANITIZER-PASS-CLEANUP-2026-05-30 in ## Recently closed.

Netflix golden-data gate (ADR-0024)

  • No assertAlmostEqual(...) golden assertions modified. Both
    fixes are bit-exact with prior behaviour on every input.
  • No CODEOWNERS exception needed.

Cross-backend numerical results

adm   cpu-vs-avx2-ULP=0   cpu-vs-avx512-ULP=0   (preserved; bit-exact)
cambi cpu-only            no GPU twins for this fix path

Performance

Not a perf change. The CAMBI fix adds two assignments and 8 bytes per
extractor instance (one per pipeline). The ADM fix is one character
per call site and produces identical machine code under any optimising
compiler with two's-complement integer representation.

Deep-dive deliverables (ADR-0108)

Reproducer

# Reproduce the original UBSan misalignment + signed-shift on master:
git checkout origin/master
meson setup build-asan core -Denable_cuda=false -Denable_sycl=false \
  -Db_sanitize=address,undefined
ninja -C build-asan

# Cambi misaligned-store (UBSan-flagged on max_log_contrast):
ASAN_OPTIONS=halt_on_error=0 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=0 \
  ./build-asan/tools/vmaf \
    --reference python/test/resource/yuv/src01_hrc00_576x324.yuv \
    --distorted python/test/resource/yuv/src01_hrc01_576x324.yuv \
    --width 576 --height 324 --pixel_format 420 --bitdepth 8 \
    --feature cambi
# expected on master: 2 runtime errors (store + load misalignment)
# expected on this PR: silent

# ADM signed-shift UB (HBD path):
./build-asan/tools/vmaf \
  --reference python/test/resource/yuv/src01_hrc00_576x324.yuv422p10le.yuv \
  --distorted python/test/resource/yuv/src01_hrc01_576x324.yuv422p10le.yuv \
  --width 576 --height 324 --pixel_format 422 --bitdepth 10 \
  --feature adm
# expected on master: "left shift of negative value -4240" per frame
# expected on this PR: silent

Findings detail

# File Finding Root cause Severity
1 core/src/feature/cambi.c (CambiState) UBSan misaligned 4-byte store + load + silent struct-field overwrite VMAF_OPT_TYPE_INT parser writes into uint16_t slot at 2-byte-aligned offset; clobbers adjacent field on window_size UB + silent struct corruption — triggered on every --feature cambi
2 core/src/feature/x86/adm_avx{2,512}.c (4 sites each) UBSan left shift of negative value Cast on shift result, not operand; signed shift of negative int is UB UB — triggered on every HBD ADM frame

Known follow-ups

  • CambiState::enc_width, enc_height, enc_bitdepth, src_width,
    src_height are unsigned but targeted by VMAF_OPT_TYPE_INT.
    Layout-compatible on every shipped ABI; UBSan does not flag. A future
    stricter option-type schema would discharge the class.
  • AdmState::adm_adm3_apply_hm is int but targeted by
    VMAF_OPT_TYPE_BOOL. Safe by accident
    (memset(priv, 0, priv_size) + 1-byte bool write). Same class.

Both noted in ADR-0869 §Consequences as follow-up audit candidates.

@lusoris
lusoris marked this pull request as ready for review May 30, 2026 20:52
@lusoris
lusoris enabled auto-merge (squash) May 30, 2026 20:52
@lusoris
lusoris marked this pull request as draft May 30, 2026 20:53
auto-merge was automatically disabled May 30, 2026 20:53

Pull request was converted to draft

@lusoris

lusoris commented May 30, 2026

Copy link
Copy Markdown
Contributor Author

Watch-loop skipped: rebase onto current origin/master produced multiple add/add conflicts including python/test/feature_extractor_test.py, quality_runner_test.py, routine_test.py (Netflix golden-data territory — agent may not touch per CLAUDE §8/§12 r1) plus docs/state.md, docs/rebase-notes.md, docs/adr/_index_fragments/_order.txt, core/test/meson.build. Manual rebase required. Loop continues to next queue PR.

@lusoris
lusoris force-pushed the fix/sanitizer-pass-cleanup branch from 9cfdc8a to 6270309 Compare May 30, 2026 21:34
@lusoris
lusoris marked this pull request as ready for review May 30, 2026 22:49
@lusoris
lusoris enabled auto-merge (squash) May 30, 2026 22:49
@lusoris lusoris closed this May 30, 2026
auto-merge was automatically disabled May 30, 2026 22:50

Pull request was closed

@lusoris
lusoris force-pushed the fix/sanitizer-pass-cleanup branch from 6270309 to bc0f002 Compare May 30, 2026 22:50
lusoris added a commit that referenced this pull request May 30, 2026
…signed-shift UB (ADR-0869)

Local ASan+UBSan audit on master tip surfaced two real UB findings
that the per-PR CI sanitizer combination did not catch.

1) core/src/feature/cambi.c: the options table declared `window_size`
   and `max_log_contrast` as VMAF_OPT_TYPE_INT, but the underlying
   CambiState fields were uint16_t. set_option_int's `*(int *)data =
   value` write produced a UBSan misaligned-store on max_log_contrast
   (2-byte-aligned slot) AND silently overwrote src_window_size with
   the upper bytes of the int when setting window_size. Mirror read
   site in vmaf_feature_name_from_options repeated the load-side
   misalignment per frame for every --feature cambi invocation.

   Fix: add shadow int slots window_size_opt / max_log_contrast_opt;
   point the options at them; copy into the existing uint16_t runtime
   fields in init(). Option-parser bounds (15..127, 0..5) guarantee
   the narrowing is lossless. Inner-loop SIMD/scalar signatures
   unchanged. Bit-exact with prior behaviour.

2) core/src/feature/x86/adm_avx{2,512}.c: DWT2 filter packing used
   `(uint32_t)(filter[k] << 16)` — cast on the shift's RESULT rather
   than its operand, so the inner shift still ran on a signed int. UB
   whenever filter[k] was negative (every HBD ADM frame; UBSan logs
   `left shift of negative value -4240`).

   Fix: move the cast inside — `((uint32_t)filter[k] << 16)`. Shifting
   an unsigned operand is fully defined; bit-exact with the original
   wrap-on-overflow signed shift on every two's-complement target.

Verification:
- ASan+UBSan unit-test suite (49 fast + 12 dnn + 2 slow = 63 tests OK)
- vmaf CLI under sanitizers on 4:2:0 8-bit, 4:2:2 10-bit, 4:2:0 12-bit
  + full feature set (psnr, ssim, ms_ssim, ciede, cambi, psnr_hvs, vif,
  adm, motion) + model load — all silent under sanitizers
- Cambi feature-name derivation produces cambi_mlc_3_ws_63 with tuned
  options (proves both shadow-slot write and read are correct)
- AVX2/AVX-512 ADM bit-identical with master on Netflix golden 10/12-bit

Restored from orphan commit 6270309 (original PR #352 head was force-
pushed away when the watch-loop mis-handled a rebase conflict; the
underlying fix is unchanged).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@lusoris lusoris reopened this May 30, 2026
@lusoris
lusoris marked this pull request as draft May 30, 2026 23:17
@lusoris

lusoris commented May 30, 2026

Copy link
Copy Markdown
Contributor Author

Recovered and reopened. The original branch was force-pushed to master tip (bc0f002f1b) at 2026-05-30T22:50:24Z when the watch-loop mis-handled the rebase-conflict at 21:25 — the underlying fix-content was lost from the branch but the orphan commit 6270309d still exists in the repo.

This force-push restores the actual fix on top of current origin/master (bc0f002f1b):

  • C-source diff is unchanged (cambi.c +24/-2, adm_avx2.c +16/-4, adm_avx512.c +16/-4).
  • Doc deliverables (ADR-0869, research digest, changelog, state.md row, rebase-notes entry, _order.txt, README index row, AGENTS.md invariant) all reconstructed.
  • Now applies cleanly without conflicts because it's properly rooted at master.

Kept as DRAFT — train-watcher will promote when ready.

@lusoris
lusoris marked this pull request as ready for review May 31, 2026 13:06
@lusoris
lusoris force-pushed the fix/sanitizer-pass-cleanup branch from eb4ed79 to 20b0d9e Compare May 31, 2026 13:06
@lusoris
lusoris merged commit 7ff6ae6 into master May 31, 2026
18 of 57 checks passed
@lusoris
lusoris deleted the fix/sanitizer-pass-cleanup branch May 31, 2026 13:06
lusoris added a commit that referenced this pull request Jun 12, 2026
…CI) (#868)

* fix(codeql): resolve HIGH-severity security-cpp-high alerts (23 sites)

- cpp/integer-multiplication-cast-to-long (11): pre-cast one operand to
  size_t / double / ptrdiff_t before int*int multiplications in
  cambi.c, float_vif.c (log message), iqa/convolve.c (img_offset),
  moment.c, psnr.c, and vif_tools.c (four memcpy size expressions).
  Add stddef.h to convolve.c for ptrdiff_t.

- cpp/incomplete-parity-check (3): change `% 2 == 1` to `% 2 != 0`
  in vif_tools.c (assert), svm.cpp (powi loop), pdjson.c (JSON
  object key/value alternation). The == 1 form is wrong for negative
  operands; != 0 is always correct.

- cpp/wrong-type-format-argument (2): fix float_vif.c error log that
  printed size_t fields scaled_w/scaled_h with %d; change to %zu.

- cpp/world-writable-file-creation (1): in vmaf.cpp replace bare
  fopen("wb") with open(O_WRONLY|O_CREAT|O_TRUNC, 0644)+fdopen() on
  POSIX so the created file is never world-writable independent of the
  caller's umask. Add <fcntl.h>.

- cpp/path-injection (4): in test_output.c resolve the mkstemp-created
  path through realpath() immediately after creation, breaking the taint
  chain from getenv("TMPDIR") to the vmaf_write_output call site.

- cpp/toctou-race-condition (2): skipped — both sites are in test
  cleanup (RMDIR after stat assertion). The stat result drives a test
  assertion, not a security-sensitive access decision; no atomic
  replacement of open() is applicable to rmdir. Reported as skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(codeql): resolve security-python-and-ci CodeQL alerts

Fixes 18 open CodeQL alerts across the Python and CI categories:

- yaml.github-actions.security.run-shell-injection (#661): move
  github.event_name, github.base_ref, and github.event.before from
  inline ${{...}} interpolation to env: vars in the SYCL clang-tidy
  detect step of lint-and-format.yml.

- python.lang.security.use-defused-xml-parse (#216, #217): replace
  xml.etree.ElementTree with defusedxml.ElementTree in
  feature_extractor.py and quality_runner.py; add defusedxml>=0.7.1
  to python/pyproject.toml and python/requirements.txt.

- py/undefined-export (#352, #353, #354, #616): restructure
  aiutils/__init__.py to do a conditional eager import of the parquet
  helpers so the names are defined when pyarrow is present, and only
  include them in __all__ when the import succeeded.

- py/stack-trace-exposure (#178, #179, #585): log exception detail
  server-side and return a generic message to the HTTP client in
  http_transport.py _handle_score (invalid JSON, bad params, scorer
  error branches).

- python.lang.security.audit.dangerous-subprocess-use-tainted-env-args
  (#227, #372): add shlex.quote() around user-supplied path arguments
  passed into shell strings in extract_ugc_features.py and
  test_bbb_e2e_v5_bug_cluster.py.

- py/file-not-closed (#677, #678): replace bare open() calls with
  context managers in test_coverage_round3.py.

- py/redundant-comparison (#427, #431): remove redundant
  assert not (x != y) lines that duplicate the preceding assert x == y.

- py/equals-hash-mismatch (#182): convert RdPoint to frozen=True
  dataclass so __eq__ and __hash__ are generated consistently.

- py/inheritance/signature-mismatch (#197): add result_dict=None
  default to EnsembleVmafQualityRunner._populate_result_dict so the
  signature is compatible with the base class.

- py/multiple-definition (#201): drop redundant assignment to
  feature_found in feature_extractor.py wildcard discovery path.

- py/str-format/surplus-named-argument (#204): remove unused
  dataset= kwarg from the format() call in routine.py.

Skipped: python.lang.security.audit.insecure-file-permissions (#373) —
  the Unix socket at 0o660 is intentional (Go sidecar node must write
  to it and runs as the same UNIX group); tightening to 0o644 would
  break the IPC channel.

Skipped: py/path-injection (#180, #181) — _validate_path() already
  resolves the path and checks it against an allowlist before any file
  operation; the data flow is secure and the CodeQL dataflow trace is a
  false positive on this allowlisted pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Lusoris <lusoris@pm.me>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@lusoris lusoris added this to the 1.0.0 — First release milestone Sep 4, 2026
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.

1 participant