Heap Overflow in EXIF writer in IFD tags processing

Pwno found a heap-buffer-overflow write in FFmpeg's avcodec/exif for due to size calculation error of non‑contiguous extra IFDs (Image File Directory), this effects .png, .jpg, .webp, .avif... results in a four bytes heap overflow (that always overwrites next chunk header data, see house-of-einherjar).

c
// libavcodec/pngdec.c 763
static int decode_exif_chunk(AVCodecContext *avctx, PNGDecContext *s,
                             GetByteContext *gb)
{
    if (!(s->hdr_state & PNG_IHDR)) {
        av_log(avctx, AV_LOG_ERROR, "eXIf before IHDR\n");
        return AVERROR_INVALIDDATA;
    }

    av_buffer_unref(&s->exif_data);
    s->exif_data = av_buffer_alloc(bytestream2_get_bytes_left(gb));
    if (!s->exif_data)
        return AVERROR(ENOMEM);
    bytestream2_get_buffer(gb, s->exif_data->data, s->exif_data->size);

    return 0;
}

Definition of the exif store (s->exif_data)

c
	// libavcodec/pngdec.c:1758
    if (s->exif_data) {
        // we swap because ff_decode_exif_attach_buffer adds to p->metadata
        FFSWAP(AVDictionary *, p->metadata, s->frame_metadata);
        ret = ff_decode_exif_attach_buffer(avctx, p, &s->exif_data, AV_EXIF_TIFF_HEADER);
        FFSWAP(AVDictionary *, p->metadata, s->frame_metadata);
        if (ret < 0) {
            av_log(avctx, AV_LOG_WARNING, "unable to attach EXIF buffer\n");
            return ret;
        }
    }

PNG decoder reads the exif chunk into PNGDecContext.exif_data in libavcodec/pngdec.c:763 That buffer becomes the EXIF payload associated with the frame in libavcodec/pngdec.c:1761

c
int ff_decode_exif_attach_buffer(AVCodecContext *avctx, AVFrame *frame, AVBufferRef **pbuf,
                                 enum AVExifHeaderMode header_mode)
{
    int ret;
    AVBufferRef *data = *pbuf;
    AVExifMetadata ifd = { 0 };

    ret = av_exif_parse_buffer(avctx, data->data, data->size, &ifd, header_mode);
    if (ret < 0)
        goto end;

    ret = exif_attach_ifd(avctx, frame, &ifd, pbuf);

ff_decode_exif_attach_buffer() parses the EXIF payload via av_exif_parse_buffer() in libavcodec/decode.c:2436 -> libavcodec/exif.c:839. The TIFF IFD list is parsed in libavcodec/exif.c:455, and each tag becomes an AVExifEntry. Extra IFDs are represented as AV_TIFF_IFD entries and, with this commit, can be stored under IDs 0xFFFC...0xFFED

c
    /* IFD tags */
    {"ExifIFD",                    0x8769}, // <- An IFD pointing to standard Exif metadata
    {"GPSInfo",                    0x8825}, // <- An IFD pointing to GPS Exif Metadata
    {"InteropIFD",                 0xA005}, // <- Table 13 Interoperability IFD Attribute Information
    {"GlobalParametersIFD",        0x0190},
    {"ProfileIFD",                 0xc6f5},
};

(libavcodec/exif.c:192 and libavcodec/exif.c:912)

c
static int exif_attach_ifd(AVCodecContext *avctx, AVFrame *frame, const AVExifMetadata *ifd, AVBufferRef **pbuf)
{
    const AVExifEntry *orient = NULL;
    AVExifMetadata *cloned = NULL;
    int ret;

    for (size_t i = 0; i < ifd->count; i++) {
        const AVExifEntry *entry = &ifd->entries[i];
        if (entry->id == av_exif_get_tag_id("Orientation") &&
            entry->count > 0 && entry->type == AV_TIFF_SHORT) {
            orient = entry;
            break;
        }
    }

    if (orient) {
        av_log(avctx, AV_LOG_DEBUG, "found EXIF orientation: %" PRIu64 "\n", orient->value.uint[0]);
        ret = attach_displaymatrix(avctx, frame, orient->value.uint[0]);
        if (ret < 0) {
            av_log(avctx, AV_LOG_WARNING, "unable to attach displaymatrix from EXIF\n");
        } else {
            cloned = av_exif_clone_ifd(ifd);
            if (!cloned) {
                ret = AVERROR(ENOMEM);
                goto end;
            }
            av_exif_remove_entry(avctx, cloned, orient->id, 0);
            ifd = cloned;
	//....

exif_attach_ifd() looks for orientation, attaches displaymatrix, removes Orientation, and reserializes the IFD (libavcodec/decode.c:2375). That reserialization is what brings the write path into a decode flow.

c
static size_t exif_get_ifd_size(const AVExifMetadata *ifd)
{
    /* 6 == 4 + 2; 2-byte entry-count at the beginning */
    /* plus 4-byte next-IFD pointer at the end */
    size_t total_size = IFD_EXTRA_SIZE;
    for (size_t i = 0; i < ifd->count; i++) {
        const AVExifEntry *entry = &ifd->entries[i];
        if (entry->type == AV_TIFF_IFD) {
            /* this is an extra IFD, not an entry, so we don't need to add base tag size */
            size_t base_size = entry->id > 0xFFECu && entry->id <= 0xFFFCu ? 0 : BASE_TAG_SIZE;
            total_size += base_size + exif_get_ifd_size(&entry->value.ifd) + entry->ifd_offset;
        } else {
            size_t payload_size = entry->count * exif_sizes[entry->type];
            total_size += BASE_TAG_SIZE + (payload_size > 4 ? payload_size : 0);

av_exif_write() allocates a buffer using exif_get_ifd_size() (libavcodec/exif.c:746 and libavcodec/exif.c:668). The new logic skips the 12‑byte base tag size for any AV_TIFF_IFD entry in 0xFFED...0xFFFC Note that here av_exif_write() tries to peel off extra IFD tags from 0xFFFC downward (libavcodec/exif.c:801). It breaks on the first missing tag. If 0xFFFC is missing but 0xFFFB exists (non‑contiguous tag), the loop stops and leaves the extra tag inside the main IFD.

c
static int exif_write_ifd(void *logctx, PutByteContext *pb, int le, int depth, const AVExifMetadata *ifd)
{
    int offset, ret, tell, tell2;
    tell = bytestream2_tell_p(pb);
    tput16(pb, le, ifd->count);
    offset = tell + IFD_EXTRA_SIZE + BASE_TAG_SIZE * (uint32_t) ifd->count;
    av_log(logctx, AV_LOG_DEBUG, "writing IFD with %u entries and initial offset %d\n", ifd->count, offset);
    for (size_t i = 0; i < ifd->count; i++) {
        const AVExifEntry *entry = &ifd->entries[i];
        av_log(logctx, AV_LOG_DEBUG, "writing TIFF entry: id: 0x%04" PRIx16 ", type: %d, count: %"
                                      PRIu32 ", offset: %d, offset value: %d\n",
                                      entry->id, entry->type, entry->count,
                                      bytestream2_tell_p(pb), offset);
        tput16(pb, le, entry->id);
        if (entry->id == MAKERNOTE_TAG && entry->type == AV_TIFF_IFD) {
            size_t ifd_size = exif_get_ifd_size(&entry->value.ifd);
            tput16(pb, le, AV_TIFF_UNDEFINED);
            tput32(pb, le, ifd_size);
        } else {
            tput16(pb, le, entry->type);
            tput32(pb, le, entry->count);
        }
        if (entry->type == AV_TIFF_IFD) {
            tput32(pb, le, offset);
            tell2 = bytestream2_tell_p(pb);
            bytestream2_seek_p(pb, offset, SEEK_SET);
            if (entry->ifd_offset)
                bytestream2_put_buffer(pb, entry->ifd_lead, entry->ifd_offset);
            ret = exif_write_ifd(logctx, pb, le, depth + 1, &entry->value.ifd);
            if (ret < 0)
                return ret;
            offset += ret + entry->ifd_offset;
            bytestream2_seek_p(pb, tell2, SEEK_SET);
        } else {
            size_t payload_size = entry->count * exif_sizes[entry->type];
            if (payload_size > 4) {
                tput32(pb, le, offset);
                tell2 = bytestream2_tell_p(pb);
                bytestream2_seek_p(pb, offset, SEEK_SET);
                exif_write_values(pb, le, entry);
                offset += payload_size;
                bytestream2_seek_p(pb, tell2, SEEK_SET);
            } else {
                /* zero uninitialized excess payload values */
                AV_WN32(pb->buffer, 0);
                exif_write_values(pb, le, entry);
                bytestream2_seek_p(pb, 4 - payload_size, SEEK_CUR);

exif_write_ifd() always writes every entry in the IFD, including the still‑present 0xFFFB entry (libavcodec/exif.c:647). Since the buffer was sized as if that entry had no 12‑byte directory slot, the buffer is too small. When it hits a small payload entry (e.g., SHORT), it executes AV_WN32(pb->buffer, 0) to zero inline padding (libavcodec/exif.c:731). At that point pb->buffer is already at pb->buffer_end - 2, so the 4‑byte zero write spills past the buffer end into the next heap chunk’s metadata.

ASan

Note that for .tiff:

Timeline