Skip to content

fix(#1980): reject a non-physical encoding white-point luminance - #1984

Merged
xsscx merged 1 commit into
masterfrom
fix-1980-encoding-white-luminance
Aug 5, 2026
Merged

fix(#1980): reject a non-physical encoding white-point luminance#1984
xsscx merged 1 commit into
masterfrom
fix-1980-encoding-white-luminance

Conversation

@colourbill-ctrl

Copy link
Copy Markdown
Contributor

Fixes #1980.

Developed from the QA branch ci-qa-issue-1980 (@xsscx, 7c8dcb71), which is where the
spec anchor and the shape of the check come from. The code here is written fresh, and
reviewing that branch turned up two things worth correcting before this lands — those are
below rather than folded in silently.

The defect

CIccDefaultEncProfileConverter::ConvertFromParams reads the colour-space white-point
luminance out of the encoding parameters and then derives the entire viewing-conditions
block from it:

icFloatNumber Lw = pParams->GetElemNumberValue(icSigCeptWhitePointLuminanceMbr, 100);
consumer site
scales the illuminant XYZ written into spectralViewingConditionsTag IccEncoding.cpp:418-420
defaults the ambient luminance La :423
scales the surround XYZ (via La) :427-437
defaults the viewing surround Lsw :441
becomes the CAM Yb parameter :449
divisor of the surround ratio :466

ICC.2:2023 12.2.3.2.6 defines wlum as a luminance in cd/m², and supplies no exception
admitting zero. The value is profile-supplied, so a crafted profile controls it.

#1817 guarded only the last row of that table. Every other consumer still read the same
value, and the CAM adapting-luminance guard added for #1950 contains only the La
consequence — after the bad value has already been written into the tag. So a negative
luminance still propagated a negative illuminant and surround XYZ into a profile that the
converter then reported as successfully built.

This validates the luminance once, where it is read, and rejects the parameters when it is
not finite and strictly positive.

Two corrections to the QA branch

1. The check's placement leaks the viewing-conditions tag

On the branch the check sits after pCond = CIccTag::Create(icSigSpectralViewingConditionsType)
but before pIcc->AttachTag(icSigSpectralViewingConditionsTag, pCond), so delete pIcc on
the reject path never reaches it. Built at that exact placement and run under ASAN with
ASAN_OPTIONS=detect_leaks=1:

Direct leak of 96 byte(s) in 1 object(s) allocated from:
    #4 CIccTag::Create(icTagTypeSignature) IccProfLib/IccTagBasic.cpp:339
    #5 CIccDefaultEncProfileConverter::ConvertFromParams IccProfLib/IccEncoding.cpp:394
SUMMARY: AddressSanitizer: 384 byte(s) leaked in 4 allocation(s).

96 bytes per reject input, once for each of the four. Worth noting that CI's own ctest runs
with detect_leaks=0, so this would not have surfaced there.

Here the read and the check are hoisted above that allocation instead, so the reject path
has nothing to release but the profile itself. Same build, same test, this placement: clean.

2. The test change dropped #1817's coverage

The branch swaps the test input from 0.0f to -1.0f and flips the expectation. That
removes zero — the value #1817 was actually about — from the suite at the same moment its
contract changes, so nothing would have recorded the change.

The regression is extended into a table of cases instead of having its input replaced:

case expected
zero (the #1817 input) icEncConvertBadParams
negative (the #1980 input) icEncConvertBadParams
NaN icEncConvertBadParams
infinite icEncConvertBadParams
100.0f icEncConvertOk, profile produced
member absent entirely icEncConvertOk, profile produced

The last two are controls: the guard must not narrow the accepted range, and the absent
member — overwhelmingly the common case, defaulting to 100 cd/m² — has to keep working.

Behaviour change

This changes the #1817 contract deliberately, and reviewers should be looking at it:

a zero white-point luminance previously converted with icEncConvertOk, having resolved
the guarded ratio to Dark surround. It is now icEncConvertBadParams.

The surround-ratio guard from #1817 is kept rather than removed. Its Lw terms are now
redundant, but its Lsw term is still load-bearing — Lsw is a separate profile-supplied
value with its own default and no equivalent precondition, so a non-finite Lsw still
resolves the ratio to 0.0f and falls through to Dark surround. The comment there now
records which of its terms this check makes redundant, so the next reader is not misled by
a rationale that no longer holds.

Verification

check result
red — library reverted to master, new test 4/4 reject cases fail, both controls pass
green 6/6 pass
full ctest, gcc Debug 150/151; the one failure is spectral-tiff-preview, a local missing imagecodecs python module, unrelated
ASAN + LSan (detect_leaks=1) clean at this placement; leak above reproduced at the branch's
-Wall -Wextra -Wpedantic -Werror, both changed TUs 0 warnings across gcc/clang × Debug/Release
preflight-safety-checks.sh 0 failures (1 skip: codeql not installed locally)
clang Release passes — confirms the isfinite guard survives optimisation

Scope

The check is deliberately confined to wlum. While reviewing the surrounding function I
found a separate, pre-existing memory-safety defect that this PR does not touch:

IccEncoding.cpp:237 takes pLumMtx from pParams->FindElemOfType(icSigCeptLumaChromaMatrixMbr, ...)
— a borrowed pointer, exactly as pWhitePt, pSurround and pSegCurve are treated in the
same function — and then deletes it at :259 while it is still in the struct's element
list. icConvertEncodingProfile deletes pParams at :666, whose destructor frees the
elements again. A profile carrying a ceptLumaChromaMatrixMbr of 9 or more values reaches
it. That is a double free on profile-controlled input and wants its own issue and its own
regression rather than being folded in here; happy to file it separately if you would like,
@xsscx.

CIccDefaultEncProfileConverter::ConvertFromParams reads the colour-space
white-point luminance out of the encoding parameters and derives the whole
viewing-conditions block from it: it scales the illuminant and surround XYZ
written into the spectralViewingConditions tag, defaults the ambient
luminance La and the viewing surround Lsw, and becomes the CAM Yb parameter
and the divisor of the surround ratio.

ICC.2:2023 12.2.3.2.6 defines 'wlum' as a luminance in cd/m2 and admits no
zero exception. The value is profile-supplied, so a crafted profile could
make it zero, negative or non-finite. #1817 guarded the surround-ratio
division against a zero, but every other consumer in the block still read
the same value, and the CAM adapting-luminance guard added for #1950
contains only the last of those consequences.

Validate the luminance once, where it is read, and reject the parameters
when it is not finite and strictly positive. The read is hoisted above the
viewing-conditions tag allocation so the rejection path has nothing to
release but the profile itself; placing the check after that allocation
leaks the tag on all four reject inputs (96 bytes each under LSan).

This deliberately changes the #1817 contract: a zero white-point luminance
previously converted with icEncConvertOk, having resolved the guarded ratio
to Dark surround, and is now icEncConvertBadParams. The surround-ratio guard
is kept - its Lsw term is still load-bearing, since Lsw is a separate
profile-supplied value with no such precondition - and its comment now
records which of its terms the new check makes redundant.

Extend the existing regression to a table of cases rather than replacing its
input: zero (the #1817 value), negative, NaN and infinite are asserted
rejected, and a plain positive luminance plus an absent member - the common
case, which defaults to 100 cd/m2 - are asserted still accepted, so the
guard cannot silently narrow the accepted range. All four reject cases fail
against master and both controls pass.
@github-actions github-actions Bot added Testing CTest, regression, or test coverage Source C or C++ source code changes Configuration Repository, CMake, YAML, JSON, or tool configuration Build Build system, CMake, compiler, or packaging pending CI checks still running labels Aug 5, 2026
@xsscx

xsscx commented Aug 5, 2026

Copy link
Copy Markdown
Member

@colourbill-ctrl Yes, Thank You. Excellent Findings!

That is a double free on profile-controlled input and wants its own issue and its own
regression rather than being folded in here; happy to file it separately if you would like,

@xsscx xsscx self-assigned this Aug 5, 2026

@xsscx xsscx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2026-08-05 18:11:03 UTC

@xsscx
xsscx merged commit ea7a710 into master Aug 5, 2026
36 checks passed
@xsscx
xsscx deleted the fix-1980-encoding-white-luminance branch August 5, 2026 21:33
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 pending CI checks still running Source C or C++ source code changes Testing CTest, regression, or test coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Research: validate ceptWhitePointLuminanceMbr

2 participants