Skip to content

Fix: divide-by-zero in CIccPcsXform::pushXyzLumToXyz on zero illuminant luminance (#1406)#1408

Merged
ChrisCoxArt merged 2 commits into
masterfrom
fix-1406-xyzlum-divide-by-zero
Jun 18, 2026
Merged

Fix: divide-by-zero in CIccPcsXform::pushXyzLumToXyz on zero illuminant luminance (#1406)#1408
ChrisCoxArt merged 2 commits into
masterfrom
fix-1406-xyzlum-divide-by-zero

Conversation

@colourbill-ctrl

Copy link
Copy Markdown
Contributor

Fixes #1406.

The defect (UBSan float-divide-by-zero)

When luminance-based PCS matching is requested (the +1000 rendering-intent flag), the CMM inserts an inverse-luminance PCS scale step. CIccPcsXform::pushXyzLumToXyz built that step as:

icFloatNumber scale = 1.0f / XYZLum[1];   // IccCmm.cpp:3048

XYZLum[1] is the destination profile's illuminant luminance (Y), from its connection conditions. A malformed profile can drive it to zero@xsscx's fuzz PoC carries a spectralViewingConditions (svcn) tag whose IlluminantXYZ is (0,0,0) — so 1.0/0 is a divide by zero. UBSan flags it, and the resulting infinite scale corrupts every PCS sample pushed through pushScale3:

IccCmm.cpp:3048:30: runtime error: division by zero
  in CIccPcsXform::pushXyzLumToXyz(IIccProfileConnectionConditions*)
  #1 pushXYZConvert -> Connect -> CheckPCSConnections -> Begin

Bisected to 3b6089e (Nov 2017).

The fix

Fall back to an identity (1.0) scale when the luminance is not a positive, finite value:

icFloatNumber scale = (std::isfinite(XYZLum[1]) && XYZLum[1] > 0.0f)
                        ? 1.0f / XYZLum[1]
                        : 1.0f;

There is no meaningful luminance normalization to undo for a degenerate illuminant, and a no-op leaves the PCS data intact rather than poisoning it with inf/NaN. std::isfinite matches the existing guard style in this file (e.g. IccCmm.cpp:1489). The sibling forward step pushXyzToXyzLum multiplies by Y and is not a divide, so it has no divide-by-zero exposure.

Regression test

iccdev.issue-1406-roundtrip-xyzlum-dbz replays the committed PoC (.github/ci/test-data/ub-roundtrip-xyzlum-dbz-1406.icc) through iccApplyProfiles with a +1000 luminance intent over the always-present smCows380_5_780.tif:

build result
master (pre-fix) FAILIccCmm.cpp:3048:30: runtime error: division by zero
this PR PASS — zero luminance handled, no UB

The float-divide-by-zero check only exists when the tool is built with -fsanitize=float-divide-by-zero (the ci-regression-container build — plain ENABLE_UBSAN does not enable it), so the test gates on the sanitizer in CMakeCache.txt and SKIPS on other builds rather than report a misleading pass — same approach as the #1342/#1343 integer-sanitizer regression.

Verified locally under a -fsanitize=float-divide-by-zero build: FAIL on master, PASS on the fix.

🤖 Generated with Claude Code

…luminant luminance (#1406)

When luminance-based PCS matching is requested (+1000 rendering intent), the CMM
inserts an inverse-luminance PCS scale step. pushXyzLumToXyz computed the scale
as 1.0f / XYZLum[1], where XYZLum[1] is the destination profile's illuminant
luminance (Y). A malformed profile can drive that Y to zero — the fuzz PoC
carries a spectralViewingConditions (svcn) tag whose IlluminantXYZ is (0,0,0) —
so 1.0/0 is a divide by zero that UBSan's float-divide-by-zero check flags,
producing an infinite scale that then corrupts every PCS sample:

  IccCmm.cpp:3048:30: runtime error: division by zero
    in CIccPcsXform::pushXyzLumToXyz(IIccProfileConnectionConditions*)
    #1 CIccPcsXform::pushXYZConvert -> Connect -> CheckPCSConnections -> Begin

Bisected to 3b6089e (Nov 2017). The fix falls back to an identity (1.0) scale
when the luminance is not a positive, finite value (std::isfinite && > 0): there
is no meaningful luminance normalization to undo, and a no-op leaves the PCS data
intact rather than poisoning it with inf/NaN.

Regression test (iccdev.issue-1406-roundtrip-xyzlum-dbz) replays the committed
PoC through iccApplyProfiles with a +1000 luminance intent and fails if the
divide-by-zero reappears. Verified under a float-divide-by-zero UBSan build: FAIL
on master (runtime error at IccCmm.cpp:3048), PASS on fix. The check only exists
when the tool is built with -fsanitize=float-divide-by-zero (the
ci-regression-container build; plain ENABLE_UBSAN does not enable it), so the
test SKIPS on other builds rather than report a misleading pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added Testing CTest, regression, or test coverage Source C or C++ source code changes Scripts Shell, PowerShell, or repository automation scripts Configuration Repository, CMake, YAML, JSON, or tool configuration Build Build system, CMake, compiler, or packaging Unix Linux, macOS, Bash, or POSIX shell scope pending CI checks still running labels Jun 17, 2026
@ChrisCoxArt ChrisCoxArt merged commit 4eda512 into master Jun 18, 2026
25 checks passed
@xsscx xsscx added the libFuzzer libFuzzer Related label Jun 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Build Build system, CMake, compiler, or packaging Configuration Repository, CMake, YAML, JSON, or tool configuration libFuzzer libFuzzer Related pending CI checks still running Scripts Shell, PowerShell, or repository automation scripts Source C or C++ source code changes Testing CTest, regression, or test coverage Unix Linux, macOS, Bash, or POSIX shell scope

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bisect 3b6089e XYZLum[1] dbz

3 participants