Skip to content

Update Decode() for ImageDecoder API 3.1.0 - #71

Merged
garbear merged 3 commits into
xbmc:Piersfrom
cinema-ONE:imagedecoder-api-3.1.0
Aug 30, 2026
Merged

garbear merged 3 commits into
xbmc:Piersfrom
cinema-ONE:imagedecoder-api-3.1.0

Conversation

@cinema-ONE

Copy link
Copy Markdown
Contributor

Readies the add-on for ImageDecoder API 3.1.0 (xbmc/xbmc#29068), which added size_t pixelBufferSize to the C++ Decode() signature.

This is a source break rather than a rebuild: the old five-argument Decode(...) override no longer matches the base virtual, so it fails to compile against the new dev-kit.

Two commits, separable. The first is the signature alone and unblocks the build. The second uses the argument for a bounds check and can be dropped if you would rather not take it now.

The check is worth having here because the loop is driven by m_height and row_stride from the JPEG rather than the height Kodi passed, and each of the m_images is tiled at a further m_width / 2 * 4 offset.

Verified by building against a Kodi tree carrying the merged API: configure and build clean, .so produced. The generated addon.xml picks up minversion="3.1.0" version="3.1.0" from the dev-kit on its own, so nothing needs hardcoding in addon.xml.in.


Written by my AI co-author (Claude Code); posted from my account.

xbmc/xbmc#29068 added size_t pixelBufferSize to the C++ Decode() signature.
The old five-argument override no longer matches the base virtual, so this
does not compile against the new dev-kit rather than merely warning.

Signature only; the argument is used in the next commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Aug 30, 2026

Copy link
Copy Markdown

Greptile Summary

Updates the add-on to ImageDecoder API 3.1.0 by accepting the new pixel-buffer-size argument and using it to reject undersized output buffers.

  • Adds pixelBufferSize to the Decode override declaration and definition.
  • Adds a per-image output-extent check before writing decoded JPEG scanlines.

Confidence Score: 3/5

This PR should not merge until the bounds check uses the exact same destination-offset calculation as the write loop.

The signature update is consistent, but the new safety check can approve a buffer smaller than the actual write extent for later images when the aggregate width is odd.

Files Needing Attention: src/MPOPicture.cpp

Security Review

The new output bounds check does not exactly mirror the guarded destination-offset expression. For odd aggregate widths, it can underestimate the write extent and leave a buffer overrun reachable. How this was verified: The check divides m_width before multiplying by image, while the write pointer multiplies first, and those integer expressions differ for reachable odd widths.

Important Files Changed

Filename Overview
src/MPOPicture.cpp Adds the buffer-size guard, but its horizontal-offset calculation can underestimate the guarded write for odd aggregate widths.
src/MPOPicture.h Updates the virtual Decode declaration to match the ImageDecoder API 3.1.0 signature.

Reviews (1): Last reviewed commit: "Refuse a decode that would not fit the o..." | Re-trigger Greptile

Comment thread src/MPOPicture.cpp Outdated
// row_stride / 3 pixels from that offset.
const size_t bytesPerPixel = (format == ADDON_IMG_FMT_A8R8G8B8) ? 4 : 3;
if (m_height == 0 || row_stride <= 0 ||
static_cast<size_t>(m_height - 1) * pitch + image * (m_width / 2 * 4) +

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Bounds check truncates offset

When an MPO has an odd aggregate m_width, this check divides the width before multiplying by image, while the guarded destination pointer multiplies first. For later images the check therefore underestimates the actual write extent, allowing the pixel loop to overrun a tightly sized output buffer. How this was verified: The two integer expressions produce different offsets for reachable odd widths.

Suggested change
static_cast<size_t>(m_height - 1) * pitch + image * (m_width / 2 * 4) +
static_cast<size_t>(m_height - 1) * pitch + image * m_width / 2 * 4 +

cinema-ONE and others added 2 commits August 30, 2026 07:34
Use the pixelBufferSize argument added in the previous commit: work out what
the copy loop actually reaches and return false rather than write past the
end.

The loop's dimensions are not always the ones Kodi passed, which is what
makes the check worth having.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The copy loop writes B,G,R and fills the fourth byte only for A8R8G8B8, so
that is the only format actually produced. Every other value fell through to
that same loop and returned true with output that does not match the request.

ADDON_IMG_FMT_A8 was the worst of them: one byte per pixel is asked for and
three are written. The previous commit's bounds check now refuses that, but
it should not be reached at all.

Latent today only because CTexture::LoadIImage() asks for A8R8G8B8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cinema-ONE

cinema-ONE commented Aug 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Greptile's security finding was right, and I introduced it — the guard's horizontal offset did not mirror the write.

The write loop computes image * m_width / 2 * 4, which evaluates as ((image * m_width) / 2) * 4. My check had image * (m_width / 2 * 4). Those diverge whenever m_width is odd and image >= 2: for m_width = 1921, image 2 reaches 7684 bytes while the check approved 7680 — so an undersized buffer could pass and the overrun stay reachable, exactly as described.

Corrected in adf5664 (was 5957720) by mirroring the expression instead of re-deriving it:

const size_t tileOffset = image * m_width / 2 * 4;

Checked the sibling PRs for the same class of error — heif and raw derive their extent from the same operands in the same order as their loops, so neither diverges.

The branch has also gained a third commit since that review: Refuse formats the add-on does not implement. The copy loop writes B,G,R and fills the fourth byte only for A8R8G8B8, so every other ADDON_IMG_FMT returned true with output that did not match the request; ADDON_IMG_FMT_A8 asked for one byte per pixel and got three.


Written by my AI co-author (Claude Code); posted from my account.

@cinema-ONE
cinema-ONE force-pushed the imagedecoder-api-3.1.0 branch from b25826d to 4e246b5 Compare August 30, 2026 05:35
@garbear
garbear merged commit 2b0e343 into xbmc:Piers Aug 30, 2026
8 checks passed
@garbear

garbear commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Thanks for the improvement!

@garbear

garbear commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

@cinema-ONE I got all the "Update Decode()" PRs, the rest might need to be rebased.

@cinema-ONE

Copy link
Copy Markdown
Contributor Author

All four are rebased already — #72, #73, #74 and xbmc/imagedecoder.heif#92 are MERGEABLE as of now.

#71 conflicted with #73 and #74 in MPOPicture.cpp (the format whitelist landed where #73's setjmp guard sits); both keep the two guards side by side, and I rebuilt against the merged API rather than assuming.

Order that reads easiest: xbmc/imagedecoder.heif#92 and #72 are the TinyEXIF resync and stand alone. #73 is the libjpeg exit() fix, no parser changes. #74 is stacked on #73, so its diff includes #73's commit — merging #73 first collapses it to two commits.


Written by my AI co-author (Claude Code); posted from my account.

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