Skip to content

Fix memory leak of shaper buffer in ParseCube() when loading .cube files#586

Merged
mm2 merged 1 commit into
mm2:masterfrom
94xhn:fix-cube-shaper-leak
Jul 19, 2026
Merged

Fix memory leak of shaper buffer in ParseCube() when loading .cube files#586
mm2 merged 1 commit into
mm2:masterfrom
94xhn:fix-cube-shaper-leak

Conversation

@94xhn

@94xhn 94xhn commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

ParseCube() (src/cmscgats.c) allocates a temporary shapers buffer to
stage the LUT_1D_SIZE tone-curve samples read from a .cube file before
handing them off to cmsBuildTabulatedToneCurveFloat() and
cmsStageAllocToneCurves():

cmsFloat32Number* shapers = (cmsFloat32Number*)_cmsMalloc(cube->ContextID, 3 * shaper_size * sizeof(cmsFloat32Number));
...
*Shaper = cmsStageAllocToneCurves(cube->ContextID, 3, curves);
cmsFreeToneCurveTriple(curves);

Both consumers copy the sample data into their own storage rather than
retaining a pointer into shapers:

  • AllocateToneCurveStruct() duplicates the segment's SampledPoints via
    _cmsDupMem().
  • cmsStageAllocToneCurves() duplicates each tone curve via
    cmsDupToneCurve().

So shapers is never referenced again after cmsFreeToneCurveTriple(curves)
runs on the line right below — but it is never freed. This leaks
3 * shaper_size * sizeof(cmsFloat32Number) bytes on every successful
load of a .cube file that carries a LUT_1D_SIZE entry, e.g. via
cmsCreateDeviceLinkFromCubeFile(THR)().

The sibling 3D-CLUT branch a few lines below already frees its equivalent
scratch buffer:

*CLUT = cmsStageAllocCLutFloat(cube->ContextID, lut_size, 3, 3, lut_table);
_cmsFree(cube->ContextID, lut_table);

This PR brings the shaper path in line with it by adding the missing
_cmsFree(cube->ContextID, shapers); call.

Verification

Built the library standalone from this source tree with
-fsanitize=address,undefined and reproduced the leak with a minimal
LUT_1D_SIZE-only .cube file loaded through the public
cmsCreateDeviceLinkFromCubeFile() API:

Direct leak of 48 byte(s) in 1 object(s) allocated from:
    #0 malloc
    #1 _cmsMallocDefaultFn src/cmserr.c:141
    #2 _cmsMalloc src/cmserr.c:311
    #3 ParseCube src/cmscgats.c:3166
    #4 cmsCreateDeviceLinkFromCubeFileTHR src/cmscgats.c:3255
    #5 cmsCreateDeviceLinkFromCubeFile src/cmscgats.c:3312

After adding the _cmsFree() call, the identical repro is clean (no leak
reported, and no use-after-free/heap-corruption from freeing the buffer
after its contents have already been copied out elsewhere — confirmed by
AddressSanitizer instrumentation on the same run). Also exercised a
combined LUT_1D_SIZE + LUT_3D_SIZE file through an actual
cmsCreateTransform()/cmsDoTransform() round trip with no
correctness regressions, and ran the existing testbed/testcms2.c
regression suite before/after with identical output.

This is unrelated to #585 (which fixes a separate Extra-channel
mis-indexing bug in cmspack.c's planar-premul unroll path).

ParseCube() allocates a temporary 'shapers' buffer to stage the LUT_1D_SIZE
tone-curve samples before handing them to cmsBuildTabulatedToneCurveFloat()
and cmsStageAllocToneCurves(). Both of those copy the sample data into their
own storage (AllocateToneCurveStruct() duplicates the segment's
SampledPoints via _cmsDupMem, and cmsStageAllocToneCurves() duplicates each
tone curve via cmsDupToneCurve), so 'shapers' is never referenced again
after cmsFreeToneCurveTriple(curves) runs -- but it was never freed either.

This leaks 3 * shaper_size * sizeof(cmsFloat32Number) bytes on every single
successful load of a .cube file that carries a LUT_1D_SIZE (1D shaper)
entry, e.g. via cmsCreateDeviceLinkFromCubeFile(THR)(). The sibling 3D CLUT
path a few lines below already frees its equivalent scratch buffer
(_cmsFree(cube->ContextID, lut_table)); this brings the shaper path in
line with it.

Verified independently: built the library from this source with
-fsanitize=address,undefined and reproduced the leak with a minimal
LUT_1D_SIZE-only .cube file loaded via cmsCreateDeviceLinkFromCubeFile();
LeakSanitizer reported exactly the 'shapers' allocation at cmscgats.c
(48 bytes for shaper_size=4). After adding the _cmsFree() call the same
repro is clean (no leak, no use-after-free/heap-corruption from freeing
the buffer after its data has been copied out). Also ran the existing
testbed/testcms2.c regression suite before/after with no behavioral
change.
@mm2
mm2 merged commit 2490d04 into mm2:master Jul 19, 2026
17 checks passed
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.

2 participants