Fix NaN bypass of AVIF_CLAMP in gain map pixel clamping#3189
Fix NaN bypass of AVIF_CLAMP in gain map pixel clamping#3189jortles wants to merge 4 commits intoAOMediaCodec:mainfrom
Conversation
powf() in avifRGBImageComputeGainMap() can return NaN when processing degenerate gain map gamma fractions. The NaN value bypasses AVIF_CLAMP(v, 0.0f, 1.0f) because IEEE 754 defines all comparisons with NaN as false, so the macro evaluates to NaN. The unclamped NaN then reaches avifSetRGBAPixel(), triggering the assertion that pixel values are in [0.0, 1.0]. Replace AVIF_CLAMP with fminf/fmaxf at the three call sites that feed pixel data into avifSetRGBAPixel(). Per C99 §7.12.12, fmaxf/fminf return the non-NaN argument when one argument is NaN, which correctly clamps NaN to 0.0f. Found via AFL++ fuzzing of the experimental gain map API.
There was a problem hiding this comment.
Pull request overview
This PR hardens the experimental gain map pipeline in src/gainmap.c so NaN values can no longer bypass the final [0,1] clamp before pixels are written. It fits into libavif’s gain map encode/apply flow by protecting the last stage of RGB output normalization in both tone mapping and gain map generation.
Changes:
- Replaces
AVIF_CLAMP(..., 0.0f, 1.0f)withfminf(1.0f, fmaxf(0.0f, ...))at the gain map code paths that feed pixel values intoavifSetRGBAPixel(). - Adds inline comments explaining why comparison-based clamping is NaN-unsafe and why
fminf/fmaxfis safer here. - Covers both the apply path (
avifRGBImageApplyGainMap) and the compute path (avifRGBImageComputeGainMap).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // NaN-safe clamp: powf() can return NaN for degenerate gamma | ||
| // values, and AVIF_CLAMP passes NaN through because IEEE 754 | ||
| // comparisons with NaN return false. fmaxf/fminf return the | ||
| // non-NaN argument per C99 §7.12.12. | ||
| gainMapF[c][(size_t)j * width + i] = fminf(1.0f, fmaxf(0.0f, v)); |
| // AVIF_CLAMP passes NaN through because IEEE 754 comparisons | ||
| // with NaN always return false. fmaxf/fminf return the non-NaN | ||
| // argument per C99 §7.12.12. | ||
| basePixelRGBA[c] = fminf(1.0f, fmaxf(0.0f, linearToGamma(basePixelRGBA[c]))); |
There was a problem hiding this comment.
Anthony: Thank you for the pull request. Please add an entry for this fix to the "Changed since 1.4.1" section in CHANGELOG.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Hi, thank you for the issue and pull request. I'm mostly curious/confused what kind of "degenerate values" cause the powf to return NaN. In the issue, you say that "A standalone reproducer is included in the linked PR." but I don't see it? And you say that your 81 byte poc is for a fuzzer that is not provided so I can't use it to reproduce. I'm wondering if the root cause might be earlier in the function, making us pass bad values to powf. |
|
You're right that the root cause is upstream of Apply path (the two sites at lines 156 and 277): when A similar NaN path exists in the compute path (line 764): if The Regarding the reproducer: apologies, that was incorrectly stated in the issue. The 81-byte PoC is consumed by a custom fuzzer harness that constructs |
Demonstrates the apply-path NaN from 0 * Inf: a black base pixel with baseOffset=0 and gainMapMax=1000 causes exp2f(1000) to overflow to +Inf, and (0.0 + 0.0) * +Inf = NaN per IEEE 754. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sRGB's avifToGammaSRGB() absorbs NaN because all branch conditions (< 0, < 0.003, < 1.0) are false for NaN, causing it to fall through to "return 1.0f". This masks the bug as silent data corruption instead of triggering the assertion in avifSetRGBAPixel(). Switch output transfer to LINEAR, whose transfer function uses AVIF_CLAMP (which also passes NaN through), allowing NaN to reach the assertion. Also fix build instructions (libavif_internal.a, libaom path). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Updated the reproducer in 608ca50. The original used sRGB output transfer, which masked the crash: Switched to Verified:
This also highlights that the NaN issue affects correctness even with sRGB — the tone-mapped output is silently wrong (1.0 instead of the correct value). |
Fixes #3188
Summary
powf()inavifRGBImageComputeGainMap()can return NaN from degenerate gain map gamma fractions.AVIF_CLAMP(v, 0.0f, 1.0f)passes NaN through because IEEE 754 comparisons with NaN always return false. The NaN then triggers the assertion inavifSetRGBAPixel()(debug builds) or causes undefined behavior in the float-to-integer conversion (release builds).Changes
Replace
AVIF_CLAMP(x, 0.0f, 1.0f)withfminf(1.0f, fmaxf(0.0f, x))at the three call sites insrc/gainmap.cthat feed pixel data intoavifSetRGBAPixel():avifRGBImageApplyGainMap(), identity copy path after color space conversionavifRGBImageApplyGainMap(), tone-mapped pixel outputavifRGBImageComputeGainMap(), gain map value afterpowf()gamma remappingPer C99 §7.12.12.2/§7.12.12.3,
fmaxf/fminfreturn the non-NaN argument when one argument is NaN, which correctly clamps NaN to 0.0f.Verification
Discovery
Found via custom AFL++ fuzzer targeting the experimental gain map API (
avifRGBImageComputeGainMap/avifRGBImageApplyGainMap). This code path is not covered by OSS-Fuzz as the build does not enable-DAVIF_ENABLE_EXPERIMENTAL_GAIN_MAP=ON.