Skip to content

fix(#2097): escape trailing-NUL signatures so XML and JSON round trip - #2103

Merged
xsscx merged 1 commit into
masterfrom
issue-2097-signature-nul-roundtrip
Aug 11, 2026
Merged

fix(#2097): escape trailing-NUL signatures so XML and JSON round trip#2103
xsscx merged 1 commit into
masterfrom
issue-2097-signature-nul-roundtrip

Conversation

@colourbill-ctrl

Copy link
Copy Markdown
Contributor

Part of #2097 — the trailing-NUL half. Confirming @xsscx's RCA in full.

The defect

icGetSigStr() (IccProfLib/IccUtil.cpp:1117) renders a signature as four
characters when it can and falls back to an unambiguous "%08Xh" escape when it
cannot. The fallback was selected only by a zero byte followed by a non-zero
one. A signature whose zeros were all trailing therefore took the
four-character path, where the zero simply terminated the C string the loop was
filling:

0x44555000  ->  "DUP"          icGetSigStr()   three characters, zero eaten
"DUP"       ->  0x44555020     icGetSigVal()   three characters right-padded 0x20

Both machine-format writers serialize tag-table identifiers through this display
helper — IccProfileXml.cpp:273 and IccProfileJson.cpp:228 — so the value was
rewritten on every round trip in either format.

Measured

Tracked fixture Testing/Encoding/ISO22028-Encoded-sRGB.xml plus one
signatureType tag carrying 0x44555000:

lane before after
ICC -> XML -> ICC 44 55 50 00 -> 44 55 50 20 byte-exact
ICC -> JSON -> ICC 44 55 50 00 -> 44 55 50 20 byte-exact
ICC -> JSON -> ICC -> XML -> ICC corrupted byte-exact

Exactly one byte moved, at offset 172. Both the correct and the corrupted
profile validate as "Profile is valid for version 5.00"
0x44555020 is
merely unregistered, not malformed — which is why the regression asserts bytes
rather than a validation report.

Checked against the live registry (registry.color.org/tag-signatures,
2026-08-11): six assigned ranges open on such a signature — 44555000 and
64757000 (Dupont), 544B0000, 546B0000, 744B0000, 746B0000
(Tektronix). All six now survive both formats.

The fix

Any zero byte selects the hex form, which icGetSigVal() parses back exactly
(its 9-character case). Every reader on both sides already routes through
icGetSigVal — XML via icXmlStrToSig, JSON directly — so this is writer-only
and symmetric.

icGetColorSigStr() carried the identical condition and is likewise a
serializer (<DataColourSpace>, <PCS>, <MCS>, <SpectralPCS>), so it gets
the same treatment.

Deliberately not addressed here

The seventh row of the issue's table, 00000000 (Sun Microsystems' assigned
range start), still round-trips to 4E554C4C. icGetSigStr(0) returns the
literal text "NULL", which is a display convention the call sites work
around by writing an empty value for zero — the #1356 / #1361 / #1843 family,
which has three regressions of its own and whose readers map empty text back to
zero. Changing it would alter iccDumpProfile output for every absent
signature, and the XML reader rejects an empty TagSignature attribute
outright, so a fix for the all-zero tag-table identifier needs a design
decision rather than a formatter change. Flagged for triage, not silently
folded in.

Regression

iccdev.issue-2097-nul-signature-roundtrip — new script test, fixture derived
from tracked XML by awk (no new binaries). Four sections: the reported example
in both formats; the five remaining registry signatures; the cross-format lane;
and an anti-overfit check — escaping every signature to hex would satisfy the
first three while making every serialized profile unreadable, so a printable
signature must still be written as its four characters. Verified red against a
reverted library (fails at the first assertion) and green on restore.

Pre-flight

profile result
clang-18 -Wall -Wextra -Wpedantic -Werror Release built 100%, 0 warnings
GCC 15.2.0 in the pinned CI container, strict + LTO (strict warnings ENABLED) built 100%, 0 warnings
clang-18 ASAN+UBSAN Debug strict built 100%, 0 warnings; new test clean under detect_leaks=1
full local ctest (serial and -j4) 175/176; the one failure is iccdev.spectral-tiff-preview, a local-only missing python imagecodecs
shellcheck 0.9.0 on the new script rc=0

The three sibling zero-signature regressions (#1356, #1361, #1843) all still
pass, confirming the "NULL" convention is untouched.

@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 passed All CI checks passed and removed pending CI checks still running labels Aug 11, 2026
@xsscx xsscx added this to the v2.3.2.3 milestone Aug 11, 2026
@xsscx xsscx linked an issue Aug 11, 2026 that may be closed by this pull request
@xsscx

xsscx commented Aug 11, 2026

Copy link
Copy Markdown
Member

Deliberately not addressed here
The seventh row of the issue's table, 00000000 (Sun Microsystems' assigned
range start), still round-trips to 4E554C4C. icGetSigStr(0) returns the
literal text "NULL", which is a display convention the call sites work
around by writing an empty value for zero — the #1356 / #1361 / #1843 family,
which has three regressions of its own and whose readers map empty text back to
zero. Changing it would alter iccDumpProfile output for every absent
signature, and the XML reader rejects an empty TagSignature attribute
outright, so a fix for the all-zero tag-table identifier needs a design
decision rather than a formatter change. Flagged for triage, not silently
folded in.

icGetSigStr() selected its unambiguous "%08Xh" escape only when a zero byte was
followed by a non-zero one.  A signature whose zeros were all trailing therefore
took the four-character path, where the zero simply terminated the C string the
loop was filling: 'DUP\0' (0x44555000) was written as "DUP", and icGetSigVal()
right-pads a three-character signature with 0x20, so it came back as 'DUP '
(0x44555020).

Confirming @xsscx's RCA.  Both machine-format writers serialize tag-table
identifiers through this display helper -- IccProfileXml.cpp:273 and
IccProfileJson.cpp:228 -- so an ICC -> XML -> ICC or ICC -> JSON -> ICC trip
silently rewrote four bytes of a conformant profile.  Measured against the
tracked ISO 22028 fixture with a 0x44555000 private tag, exactly one byte
changed (offset 172, 00 -> 20) in both lanes, and both the original and the
round-tripped profile report "Profile is valid for version 5.00": 0x44555020 is
unregistered, not malformed.

Any zero byte now takes the hex form, which icGetSigVal() parses back exactly.
That covers the six ranges the live tag registry opens on such a signature
(44555000 and 64757000 for Dupont, 544B0000, 546B0000, 744B0000 and 746B0000
for Tektronix, checked 2026-08-11).  icGetColorSigStr() carried the same
condition and is likewise a serializer -- it renders <DataColourSpace>, <PCS>
and <MCS> -- so it gets the same treatment.

The seventh row of the issue's table, a signature that is zero in all four
bytes, is deliberately not addressed here: icGetSigStr(0) returns the literal
"NULL", a display convention the call sites work around by writing an empty
value for zero, which is the #1356 / #1361 / #1843 family with its own three
regressions.  Changing it would alter iccDumpProfile output for every absent
signature, and the XML reader rejects an empty TagSignature outright, so that
row needs a design decision rather than a formatter change.

The new regression asserts byte-exact round trips for all six signatures across
both formats and the cross-format lane, plus one anti-overfit check: escaping
every signature to hex would satisfy the other assertions while making every
serialized profile unreadable, so a printable signature must still be written as
its four characters.
@xsscx
xsscx force-pushed the issue-2097-signature-nul-roundtrip branch from 09d4f89 to 61caed9 Compare August 11, 2026 10:20
@xsscx
xsscx enabled auto-merge (squash) August 11, 2026 10:21

@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-11 10:21:06 UTC

@github-actions github-actions Bot added pending CI checks still running and removed passed All CI checks passed labels Aug 11, 2026
@xsscx xsscx linked an issue Aug 11, 2026 that may be closed by this pull request
@xsscx

xsscx commented Aug 11, 2026

Copy link
Copy Markdown
Member

PR Status

2026-08-11 10:26:35 UTC

Monitoring

Potentially a transient error... waiting on the job to complete and review logs..

CI Error

Setting up auth
Fetching the repository
Removing auth
Error: The process 'C:\Program Files\Git\bin\git.exe' failed with exit code 128

@xsscx
xsscx merged commit ce9f31e into master Aug 11, 2026
60 of 62 checks passed
@xsscx

xsscx commented Aug 11, 2026

Copy link
Copy Markdown
Member

PR Status

2026-08-11 10:37:11 UTC

  1. CI Transient Issue
  2. PR Code Review is 100% GOOD on Local W5 WinTel

@xsscx
xsscx deleted the issue-2097-signature-nul-roundtrip branch August 11, 2026 10:47
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 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.

Research: Registered signatures ending in NUL bytes

2 participants