Commit 182bb23bf8 for ffmpeg
commit 182bb23bf84869e8487a5b5c6253fc9e9c26adfc
Author: luoye-cpu <3412829159@qq.com>
Date: Fri Oct 30 06:42:22 2026 +0800
avutil/hdr_dynamic_metadata: reject oversized HDR10+ counts
av_dynamic_hdr_plus_to_t35() uses caller-supplied counts as loop
bounds over fixed-size arrays. A count slightly above an array
length can make the write loop read out of bounds while the
payload remains well within AV_HDR_PLUS_MAX_PAYLOAD_SIZE.
Reject counts above the array bounds with AVERROR(EINVAL), since
these are caller-supplied fields rather than data parsed off the
wire. The checks are gated like the reads they guard: the
targeted-system-display and mastering-display peak-luminance
counts and the bezier anchors are only read when their respective
flags are set. Rejecting oversized counts also keeps the payload
within AV_HDR_PLUS_MAX_PAYLOAD_SIZE, but the size assertion is not
a substitute for validating the array bounds.
FATE cases cover every rejected count and, for gated counts,
out-of-range values with their flags clear, which must still
serialize.
Signed-off-by: luoye-cpu <3412829159@qq.com>
diff --git a/libavutil/hdr_dynamic_metadata.c b/libavutil/hdr_dynamic_metadata.c
index 9b46499343..d52470da6d 100644
--- a/libavutil/hdr_dynamic_metadata.c
+++ b/libavutil/hdr_dynamic_metadata.c
@@ -244,6 +244,41 @@ int av_dynamic_hdr_plus_to_t35(const AVDynamicHDRPlus *s, uint8_t **data, size_t
return AVERROR(EINVAL);
if ((!data || *data) && !size)
return AVERROR(EINVAL);
+ /* The caps put the payload at exactly 907 bytes; the assert cannot fire. */
+ if (s->num_windows > FF_ARRAY_ELEMS(s->params))
+ return AVERROR(EINVAL);
+
+ for (int w = 0; w < s->num_windows; w++) {
+ const AVHDRPlusColorTransformParams *p = &s->params[w];
+
+ if (p->num_distribution_maxrgb_percentiles >
+ FF_ARRAY_ELEMS(p->distribution_maxrgb))
+ return AVERROR(EINVAL);
+
+ /* The anchors are only read while tone mapping is enabled. */
+ if (p->tone_mapping_flag &&
+ p->num_bezier_curve_anchors >
+ FF_ARRAY_ELEMS(p->bezier_curve_anchors))
+ return AVERROR(EINVAL);
+ }
+
+ /* The peak-luminance pairs use a row count and a column count, and are
+ * only read when their flag is set, so check them the same way. */
+ if (s->targeted_system_display_actual_peak_luminance_flag) {
+ if (s->num_rows_targeted_system_display_actual_peak_luminance >
+ FF_ARRAY_ELEMS(s->targeted_system_display_actual_peak_luminance) ||
+ s->num_cols_targeted_system_display_actual_peak_luminance >
+ FF_ARRAY_ELEMS(s->targeted_system_display_actual_peak_luminance[0]))
+ return AVERROR(EINVAL);
+ }
+
+ if (s->mastering_display_actual_peak_luminance_flag) {
+ if (s->num_rows_mastering_display_actual_peak_luminance >
+ FF_ARRAY_ELEMS(s->mastering_display_actual_peak_luminance) ||
+ s->num_cols_mastering_display_actual_peak_luminance >
+ FF_ARRAY_ELEMS(s->mastering_display_actual_peak_luminance[0]))
+ return AVERROR(EINVAL);
+ }
/**
* Buffer size per CTA-861-H p.253-254:
diff --git a/libavutil/tests/hdr_dynamic_metadata.c b/libavutil/tests/hdr_dynamic_metadata.c
index e85604e4f0..dc0501333d 100644
--- a/libavutil/tests/hdr_dynamic_metadata.c
+++ b/libavutil/tests/hdr_dynamic_metadata.c
@@ -486,6 +486,98 @@ static void test_hdr_plus(void)
printf("to_t35 no data no size: ret==AVERROR(EINVAL)=%s\n",
ret == AVERROR(EINVAL) ? "yes" : "no");
+ /* A num_windows above the params[] length must be refused. */
+ hdr->num_windows = FF_ARRAY_ELEMS(hdr->params) + 1;
+ size = 0;
+ ret = av_dynamic_hdr_plus_to_t35(hdr, NULL, &size);
+ printf("to_t35 num_windows>3: ret==AVERROR(EINVAL)=%s\n",
+ ret == AVERROR(EINVAL) ? "yes" : "no");
+ hdr->num_windows = 2;
+
+ uint8_t pct = hdr->params[0].num_distribution_maxrgb_percentiles;
+ uint8_t anch = hdr->params[0].num_bezier_curve_anchors;
+ uint8_t trow = hdr->num_rows_targeted_system_display_actual_peak_luminance;
+ uint8_t tcol = hdr->num_cols_targeted_system_display_actual_peak_luminance;
+ uint8_t mrow = hdr->num_rows_mastering_display_actual_peak_luminance;
+ uint8_t mcol = hdr->num_cols_mastering_display_actual_peak_luminance;
+
+ hdr->params[0].num_distribution_maxrgb_percentiles =
+ FF_ARRAY_ELEMS(hdr->params[0].distribution_maxrgb) + 1;
+ size = 0;
+ ret = av_dynamic_hdr_plus_to_t35(hdr, NULL, &size);
+ printf("to_t35 percentiles>15: ret==AVERROR(EINVAL)=%s\n",
+ ret == AVERROR(EINVAL) ? "yes" : "no");
+ hdr->params[0].num_distribution_maxrgb_percentiles = pct;
+
+ hdr->params[0].num_bezier_curve_anchors =
+ FF_ARRAY_ELEMS(hdr->params[0].bezier_curve_anchors) + 1;
+ size = 0;
+ ret = av_dynamic_hdr_plus_to_t35(hdr, NULL, &size);
+ printf("to_t35 anchors>15: ret==AVERROR(EINVAL)=%s\n",
+ ret == AVERROR(EINVAL) ? "yes" : "no");
+ hdr->params[0].num_bezier_curve_anchors = anch;
+
+ /* The peak-luminance pairs are only checked when their flags are set. */
+ hdr->num_rows_targeted_system_display_actual_peak_luminance =
+ FF_ARRAY_ELEMS(hdr->targeted_system_display_actual_peak_luminance) + 1;
+ size = 0;
+ ret = av_dynamic_hdr_plus_to_t35(hdr, NULL, &size);
+ printf("to_t35 targeted rows>25: ret==AVERROR(EINVAL)=%s\n",
+ ret == AVERROR(EINVAL) ? "yes" : "no");
+ hdr->num_rows_targeted_system_display_actual_peak_luminance = trow;
+
+ hdr->num_cols_targeted_system_display_actual_peak_luminance =
+ FF_ARRAY_ELEMS(hdr->targeted_system_display_actual_peak_luminance[0]) + 1;
+ size = 0;
+ ret = av_dynamic_hdr_plus_to_t35(hdr, NULL, &size);
+ printf("to_t35 targeted cols>25: ret==AVERROR(EINVAL)=%s\n",
+ ret == AVERROR(EINVAL) ? "yes" : "no");
+ hdr->num_cols_targeted_system_display_actual_peak_luminance = tcol;
+
+ hdr->num_rows_mastering_display_actual_peak_luminance =
+ FF_ARRAY_ELEMS(hdr->mastering_display_actual_peak_luminance) + 1;
+ size = 0;
+ ret = av_dynamic_hdr_plus_to_t35(hdr, NULL, &size);
+ printf("to_t35 mastering rows>25: ret==AVERROR(EINVAL)=%s\n",
+ ret == AVERROR(EINVAL) ? "yes" : "no");
+ hdr->num_rows_mastering_display_actual_peak_luminance = mrow;
+
+ hdr->num_cols_mastering_display_actual_peak_luminance =
+ FF_ARRAY_ELEMS(hdr->mastering_display_actual_peak_luminance[0]) + 1;
+ size = 0;
+ ret = av_dynamic_hdr_plus_to_t35(hdr, NULL, &size);
+ printf("to_t35 mastering cols>25: ret==AVERROR(EINVAL)=%s\n",
+ ret == AVERROR(EINVAL) ? "yes" : "no");
+ hdr->num_cols_mastering_display_actual_peak_luminance = mcol;
+
+ /* With the flags clear, oversized counts must still serialize. */
+ hdr->targeted_system_display_actual_peak_luminance_flag = 0;
+ hdr->mastering_display_actual_peak_luminance_flag = 0;
+ hdr->num_rows_targeted_system_display_actual_peak_luminance =
+ FF_ARRAY_ELEMS(hdr->targeted_system_display_actual_peak_luminance) + 1;
+ size = 0;
+ ret = av_dynamic_hdr_plus_to_t35(hdr, NULL, &size);
+ printf("to_t35 rows>25 with flag clear: ret==0=%s\n",
+ ret == 0 ? "yes" : "no");
+ hdr->num_rows_targeted_system_display_actual_peak_luminance = trow;
+
+ hdr->num_rows_mastering_display_actual_peak_luminance =
+ FF_ARRAY_ELEMS(hdr->mastering_display_actual_peak_luminance) + 1;
+ size = 0;
+ ret = av_dynamic_hdr_plus_to_t35(hdr, NULL, &size);
+ printf("to_t35 mastering rows>25 with flag clear: ret==0=%s\n",
+ ret == 0 ? "yes" : "no");
+ hdr->num_rows_mastering_display_actual_peak_luminance = mrow;
+
+ hdr->params[0].tone_mapping_flag = 0;
+ hdr->params[0].num_bezier_curve_anchors =
+ FF_ARRAY_ELEMS(hdr->params[0].bezier_curve_anchors) + 1;
+ size = 0;
+ ret = av_dynamic_hdr_plus_to_t35(hdr, NULL, &size);
+ printf("to_t35 anchors>15 with flag clear: ret==0=%s\n",
+ ret == 0 ? "yes" : "no");
+ hdr->params[0].num_bezier_curve_anchors = anch;
+
ret = av_dynamic_hdr_plus_from_t35(NULL, (const uint8_t *)"", 0);
printf("from_t35 NULL s: ret==AVERROR(EINVAL)=%s\n",
ret == AVERROR(EINVAL) ? "yes" : "no");
diff --git a/tests/ref/fate/hdr_dynamic_metadata b/tests/ref/fate/hdr_dynamic_metadata
index 118f69d7e5..3ec298fc17 100644
--- a/tests/ref/fate/hdr_dynamic_metadata
+++ b/tests/ref/fate/hdr_dynamic_metadata
@@ -60,6 +60,16 @@ decoded:
Testing error paths
to_t35 NULL s: ret==AVERROR(EINVAL)=yes
to_t35 no data no size: ret==AVERROR(EINVAL)=yes
+to_t35 num_windows>3: ret==AVERROR(EINVAL)=yes
+to_t35 percentiles>15: ret==AVERROR(EINVAL)=yes
+to_t35 anchors>15: ret==AVERROR(EINVAL)=yes
+to_t35 targeted rows>25: ret==AVERROR(EINVAL)=yes
+to_t35 targeted cols>25: ret==AVERROR(EINVAL)=yes
+to_t35 mastering rows>25: ret==AVERROR(EINVAL)=yes
+to_t35 mastering cols>25: ret==AVERROR(EINVAL)=yes
+to_t35 rows>25 with flag clear: ret==0=yes
+to_t35 mastering rows>25 with flag clear: ret==0=yes
+to_t35 anchors>15 with flag clear: ret==0=yes
from_t35 NULL s: ret==AVERROR(EINVAL)=yes
from_t35 oversized: ret==AVERROR(EINVAL)=yes