1
0
mirror of https://git.ffmpeg.org/ffmpeg.git synced 2026-10-24 02:09:03 +03:00
Files
ffmpeg/libavcodec/fflcms2.c
David Tolnay 705286a8a7 avcodec/fflcms2: fix memory leak after cmsDeleteContext(NULL)
At the time that this code was introduced into FFmpeg in April 2022 by commit
b9a25963f7, it was correct as written. The latest release of
lcms2 at the time was 2.13.1 in which `cmsDeleteContext(NULL)` was a no-op.

Since lcms2 version 2.15, released Mar 2023, the same call is no longer a no-op and
instead means destroy the global context. The new behavior is justifiable as being
consistent with the rest of lcms2's API, where passing NULL for a cmsContext argument
means operate on the global context. For example `cmsDupContext(NULL, ...)` constructs a
context duplicated from the global context; `cmsPluginTHR(NULL, ...)` registers a plugin
into the global context.

The upstream commit that introduced the behavior change for `cmsDeleteContext` is:
a9e4601ceb

In FFmpeg, the new behavior leads to a memory leak if `ff_icc_context_uninit` is ever
called on an `FFIccContext` without `ff_icc_context_init`. Here is why that can happen,
and here is how it leads to a memory leak.

Naively, uninitializing something which was never initialized seems like it shouldn't
happen, but in this case these contexts are *lazily* initialized and *unconditionally*
deinitialized. `FFIccContext` is a member of `AVCodecInternal`:

    // libavcodec/internal.h:148-150
    #if CONFIG_LCMS2
        FFIccContext icc; /* used to read and write embedded ICC profiles */
    #endif

It is initialized conditionally by the snippet of `detect_colorspace` shown below, or by
similary shaped code in libavcodec/encode.c. The `avci->icc.avctx` guards "has it already
been initialized". The early returns beforehand are codepaths for which `icc` will not be
initialized, and will remain all-zero as allocated by `av_mallocz` in
`ff_decode_internal_alloc`.

    // libavcodec/decode.c:534-542
    if (!(avctx->flags2 & AV_CODEC_FLAG2_ICC_PROFILES))
        return 0;
    sd = av_frame_get_side_data(frame, AV_FRAME_DATA_ICC_PROFILE);
    if (!sd || !sd->size)
        return 0;
    if (!avci->icc.avctx) {
        ret = ff_icc_context_init(&avci->icc, avctx);

Later, `avci->icc` is uninitialized unconditionally by `ff_codec_close`:

    // libavcodec/avcodec.c:447-480
    if (avcodec_is_open(avctx)) {
        ...
    #if CONFIG_LCMS2
        ff_icc_context_uninit(&avci->icc);
    #endif

So every codec closed without `AV_CODEC_FLAGS2_ICC_PROFILES`, or without an ICC side-data
packet, reaches `ff_icc_context_uninit` on a never-initialized context.

As part of `cmsDeleteContext(NULL)` destroying lcms2's global context, it runs
`cmsUnregisterPlugins()` and destroys `globalContext.MemPool`. Since the global context is
shared by every user of lcms2 in the process, closing an FFmpeg codec silently tears down
unrelated libraries' lcms2 state. The most visible symptom is that
`_cmsRegisterMutexPlugin(NULL, NULL)` nulls out the mutex callbacks, after which
`cmsCloseProfile()` leaks each profile's `UsrMutex` and profiles opened later are left
without any locking at all.

The fix is to delete only contexts created by FFmpeg.

Signed-off-by: David Tolnay <dtolnay@gmail.com>
2026-09-08 23:59:36 +00:00

361 lines
12 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright (c) 2022 Niklas Haas
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "libavutil/csp.h"
#include "fflcms2.h"
static void log_cb(cmsContext ctx, cmsUInt32Number error, const char *str)
{
FFIccContext *s = cmsGetContextUserData(ctx);
av_log(s->avctx, AV_LOG_ERROR, "lcms2: [%"PRIu32"] %s\n", error, str);
}
int ff_icc_context_init(FFIccContext *s, void *avctx)
{
memset(s, 0, sizeof(*s));
s->avctx = avctx;
s->ctx = cmsCreateContext(NULL, s);
if (!s->ctx)
return AVERROR(ENOMEM);
cmsSetLogErrorHandlerTHR(s->ctx, log_cb);
return 0;
}
void ff_icc_context_uninit(FFIccContext *s)
{
for (int i = 0; i < FF_ARRAY_ELEMS(s->curves); i++)
cmsFreeToneCurve(s->curves[i]);
if (s->ctx)
cmsDeleteContext(s->ctx);
memset(s, 0, sizeof(*s));
}
static int get_curve(FFIccContext *s, enum AVColorTransferCharacteristic trc,
cmsToneCurve **out_curve)
{
if ((unsigned)trc < AVCOL_TRC_NB && s->curves[trc])
goto done;
switch (trc) {
case AVCOL_TRC_LINEAR:
s->curves[trc] = cmsBuildGamma(s->ctx, 1.0);
break;
case AVCOL_TRC_GAMMA22:
s->curves[trc] = cmsBuildGamma(s->ctx, 2.2);
break;
case AVCOL_TRC_GAMMA28:
s->curves[trc] = cmsBuildGamma(s->ctx, 2.8);
break;
case AVCOL_TRC_BT709:
case AVCOL_TRC_SMPTE170M:
case AVCOL_TRC_BT2020_10:
case AVCOL_TRC_BT2020_12:
s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 4, (double[5]) {
/* γ = */ 1/0.45,
/* a = */ 1/1.099296826809442,
/* b = */ 1 - 1/1.099296826809442,
/* c = */ 1/4.5,
/* d = */ 4.5 * 0.018053968510807,
});
break;
case AVCOL_TRC_SMPTE240M:
s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 4, (double[5]) {
/* γ = */ 1/0.45,
/* a = */ 1/1.1115,
/* b = */ 1 - 1/1.1115,
/* c = */ 1/4.0,
/* d = */ 4.0 * 0.0228,
});
break;
case AVCOL_TRC_LOG:
s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 8, (double[5]) {
/* a = */ 1.0,
/* b = */ 10.0,
/* c = */ 2.0,
/* d = */ -1.0,
/* e = */ 0.0
});
break;
case AVCOL_TRC_LOG_SQRT:
s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 8, (double[5]) {
/* a = */ 1.0,
/* b = */ 10.0,
/* c = */ 2.5,
/* d = */ -1.0,
/* e = */ 0.0
});
break;
case AVCOL_TRC_IEC61966_2_1:
s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 4, (double[5]) {
/* γ = */ 2.4,
/* a = */ 1/1.055,
/* b = */ 1 - 1/1.055,
/* c = */ 1/12.92,
/* d = */ 12.92 * 0.0031308,
});
break;
case AVCOL_TRC_SMPTE428:
s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 2, (double[3]) {
/* γ = */ 2.6,
/* a = */ pow(52.37/48.0, 1/2.6),
/* b = */ 0.0
});
break;
/* Can't be represented using the existing parametric tone curves.
* FIXME: use cmsBuildTabulatedToneCurveFloat instead */
case AVCOL_TRC_IEC61966_2_4:
case AVCOL_TRC_BT1361_ECG:
case AVCOL_TRC_SMPTE2084:
case AVCOL_TRC_ARIB_STD_B67:
case AVCOL_TRC_V_LOG:
return AVERROR_PATCHWELCOME;
default:
return AVERROR_INVALIDDATA;
}
if (!s->curves[trc])
return AVERROR(ENOMEM);
done:
*out_curve = s->curves[trc];
return 0;
}
int ff_icc_profile_generate(FFIccContext *s,
enum AVColorPrimaries color_prim,
enum AVColorTransferCharacteristic color_trc,
cmsHPROFILE *out_profile)
{
cmsToneCurve *tonecurve;
const AVColorPrimariesDesc *prim;
int ret;
if (!(prim = av_csp_primaries_desc_from_id(color_prim)))
return AVERROR_INVALIDDATA;
if ((ret = get_curve(s, color_trc, &tonecurve)) < 0)
return ret;
*out_profile = cmsCreateRGBProfileTHR(s->ctx,
&(cmsCIExyY) { av_q2d(prim->wp.x), av_q2d(prim->wp.y), 1.0 },
&(cmsCIExyYTRIPLE) {
.Red = { av_q2d(prim->prim.r.x), av_q2d(prim->prim.r.y), 1.0 },
.Green = { av_q2d(prim->prim.g.x), av_q2d(prim->prim.g.y), 1.0 },
.Blue = { av_q2d(prim->prim.b.x), av_q2d(prim->prim.b.y), 1.0 },
},
(cmsToneCurve *[3]) { tonecurve, tonecurve, tonecurve }
);
return *out_profile == NULL ? AVERROR(ENOMEM) : 0;
}
int ff_icc_profile_attach(FFIccContext *s, cmsHPROFILE profile, AVFrame *frame)
{
cmsUInt32Number size;
AVBufferRef *buf;
if (!cmsSaveProfileToMem(profile, NULL, &size))
return AVERROR_EXTERNAL;
buf = av_buffer_alloc(size);
if (!buf)
return AVERROR(ENOMEM);
if (!cmsSaveProfileToMem(profile, buf->data, &size) || size != buf->size) {
av_buffer_unref(&buf);
return AVERROR_EXTERNAL;
}
if (!av_frame_new_side_data_from_buf(frame, AV_FRAME_DATA_ICC_PROFILE, buf)) {
av_buffer_unref(&buf);
return AVERROR(ENOMEM);
}
return 0;
}
static av_always_inline void XYZ_xy(cmsCIEXYZ XYZ, AVCIExy *xy)
{
double k = 1.0 / (XYZ.X + XYZ.Y + XYZ.Z);
xy->x = av_d2q(k * XYZ.X, 100000);
xy->y = av_d2q(k * XYZ.Y, 100000);
}
static av_always_inline AVRational abs_sub_q(AVRational r1, AVRational r2)
{
AVRational diff = av_sub_q(r1, r2);
/* denominator assumed to be positive */
return av_make_q(abs(diff.num), diff.den);
}
static const AVCIExy wp_d50 = { {3457, 10000}, {3585, 10000} }; /* CIE D50 */
int ff_icc_profile_sanitize(FFIccContext *s, cmsHPROFILE profile)
{
cmsCIEXYZ *white, fixed;
AVCIExy wpxy;
AVRational diff, z;
if (!profile)
return 0;
if (cmsGetEncodedICCversion(profile) >= 0x4000000) { // ICC v4
switch (cmsGetHeaderRenderingIntent(profile)) {
case INTENT_RELATIVE_COLORIMETRIC:
case INTENT_ABSOLUTE_COLORIMETRIC: ;
/* ICC v4 colorimetric profiles are specified to always use D50
* media white point, anything else is a violation of the spec.
* Sadly, such profiles are incredibly common (Apple...), so make
* an effort to fix them. */
if (!(white = cmsReadTag(profile, cmsSigMediaWhitePointTag)))
return AVERROR_INVALIDDATA;
XYZ_xy(*white, &wpxy);
diff = av_add_q(abs_sub_q(wpxy.x, wp_d50.x), abs_sub_q(wpxy.y, wp_d50.y));
if (av_cmp_q(diff, av_make_q(1, 1000)) > 0) {
av_log(s->avctx, AV_LOG_WARNING, "Invalid colorimetric ICCv4 "
"profile media white point tag (expected %.4f %.4f, "
"got %.4f %.4f)\n",
av_q2d(wp_d50.x), av_q2d(wp_d50.y),
av_q2d(wpxy.x), av_q2d(wpxy.y));
/* x+y+z = 1 */
z = av_sub_q(av_sub_q(av_make_q(1, 1), wp_d50.x), wp_d50.y);
fixed.X = av_q2d(av_div_q(wp_d50.x, wp_d50.y)) * white->Y;
fixed.Y = white->Y;
fixed.Z = av_q2d(av_div_q(z, wp_d50.y)) * white->Y;
if (!cmsWriteTag(profile, cmsSigMediaWhitePointTag, &fixed))
return AVERROR_EXTERNAL;
}
break;
default: break;
}
}
return 0;
}
int ff_icc_profile_read_primaries(FFIccContext *s, cmsHPROFILE profile,
AVColorPrimariesDesc *out_primaries)
{
static const uint8_t testprimaries[4][3] = {
{ 0xFF, 0, 0 }, /* red */
{ 0, 0xFF, 0 }, /* green */
{ 0, 0, 0xFF }, /* blue */
{ 0xFF, 0xFF, 0xFF }, /* white */
};
AVWhitepointCoefficients *wp = &out_primaries->wp;
AVPrimaryCoefficients *prim = &out_primaries->prim;
cmsFloat64Number prev_adapt;
cmsHPROFILE xyz;
cmsHTRANSFORM tf;
cmsCIEXYZ dst[4];
xyz = cmsCreateXYZProfileTHR(s->ctx);
if (!xyz)
return AVERROR(ENOMEM);
/* We need to use an unadapted observer to get the raw values */
prev_adapt = cmsSetAdaptationStateTHR(s->ctx, 0.0);
tf = cmsCreateTransformTHR(s->ctx, profile, TYPE_RGB_8, xyz, TYPE_XYZ_DBL,
INTENT_ABSOLUTE_COLORIMETRIC,
/* Note: These flags mostly don't do anything
* anyway, but specify them regardless */
cmsFLAGS_NOCACHE |
cmsFLAGS_NOOPTIMIZE |
cmsFLAGS_LOWRESPRECALC |
cmsFLAGS_GRIDPOINTS(2));
cmsSetAdaptationStateTHR(s->ctx, prev_adapt);
cmsCloseProfile(xyz);
if (!tf) {
av_log(s->avctx, AV_LOG_ERROR, "Invalid ICC profile (e.g. CMYK)\n");
return AVERROR_INVALIDDATA;
}
cmsDoTransform(tf, testprimaries, dst, 4);
cmsDeleteTransform(tf);
XYZ_xy(dst[0], &prim->r);
XYZ_xy(dst[1], &prim->g);
XYZ_xy(dst[2], &prim->b);
XYZ_xy(dst[3], wp);
return 0;
}
int ff_icc_profile_detect_transfer(FFIccContext *s, cmsHPROFILE profile,
enum AVColorTransferCharacteristic *out_trc)
{
/* 8-bit linear grayscale ramp */
static const uint8_t testramp[16][3] = {
{ 1, 1, 1}, /* avoid exact zero due to log100 etc. */
{ 17, 17, 17},
{ 34, 34, 34},
{ 51, 51, 51},
{ 68, 68, 68},
{ 85, 85, 85},
{ 02, 02, 02},
{119, 119, 119},
{136, 136, 136},
{153, 153, 153},
{170, 170, 170},
{187, 187, 187},
{204, 204, 204},
{221, 221, 221},
{238, 238, 238},
{255, 255, 255},
};
double dst[FF_ARRAY_ELEMS(testramp)];
for (enum AVColorTransferCharacteristic trc = 0; trc < AVCOL_TRC_NB; trc++) {
cmsToneCurve *tonecurve;
cmsHPROFILE ref;
cmsHTRANSFORM tf;
double delta = 0.0;
if (get_curve(s, trc, &tonecurve) < 0)
continue;
ref = cmsCreateGrayProfileTHR(s->ctx, cmsD50_xyY(), tonecurve);
if (!ref)
return AVERROR(ENOMEM);
tf = cmsCreateTransformTHR(s->ctx, profile, TYPE_RGB_8, ref, TYPE_GRAY_DBL,
INTENT_RELATIVE_COLORIMETRIC,
cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE);
cmsCloseProfile(ref);
if (!tf) {
av_log(s->avctx, AV_LOG_ERROR, "Invalid ICC profile (e.g. CMYK)\n");
return AVERROR_INVALIDDATA;
}
cmsDoTransform(tf, testramp, dst, FF_ARRAY_ELEMS(dst));
cmsDeleteTransform(tf);
for (int i = 0; i < FF_ARRAY_ELEMS(dst); i++)
delta += fabs(testramp[i][0] / 255.0 - dst[i]);
if (delta < 0.01) {
*out_trc = trc;
return 0;
}
}
*out_trc = AVCOL_TRC_UNSPECIFIED;
return 0;
}