png_set_quantize: validate palette count parameters - #907
png_set_quantize: validate palette count parameters#907gamedeveloper8136 wants to merge 5 commits into
Conversation
num_palette and maximum_colors are consumed as allocation sizes, loop bounds, array indices, and a memcpy length, but no range check exists before the PNG_QUANTIZE transform is enabled. The internal quantize_index array and the owned palette copy are sized for the PNG palette maximum (PNG_MAX_PALETTE_LENGTH), so an out-of-range count (num_palette <= 0, num_palette > PNG_MAX_PALETTE_LENGTH, or maximum_colors <= 0) can cause out-of-bounds accesses and oversized copies. Reject such calls with a warning before the transform is enabled, matching the validation already used by png_set_PLTE, png_set_hIST, and png_set_sCAL. Valid calls are unaffected. Add a pnggetset regression test covering the out-of-range boundary values and a valid call.
|
gamedeveloper, onlybug; can you provide a repro? |
Provide a self-contained program (repro_quantize.c) that calls png_set_quantize() with out-of-range palette counts and a guard-page malloc interposer (guard_malloc.c) that makes the resulting out-of-bounds accesses fault deterministically without ASan.
jbowler
left a comment
There was a problem hiding this comment.
In the documentation for png_set_quantize (from libpng(3), i.e. man 3 libpng) Glenn wrote:
If you pass a palette that is larger than maximum_colors the file will reduce the number of colors in the palette so it will fit into maximum_colors.
In your test case (line 842 of pnggetset.c) you test the "oversized palette" case by passing in 257 to both parameters, but 257 for num_palette should be valid; it's only if maximum_colors is greater than 1<<output_bit_depth (not PNG_MAX_PALETTE_LENGTH I think) there are should be a problem. The code at line 5134 (if (num_palette > maximum_colors) is meant to handle this but without a valid test case we can't be sure.
So the test needs to check maximum_palette, not num_palette. I'm not sure how the output bit depth gets set; that controls how many colors are possible, it should come from maximum_colors.
The png_warning and probably the two preceding return statements needs to be png_app_error because the app is going to assume the palette mapping works and most likely will allocate row buffers to match. The app will crash (well, in fact, libpng will crash) if png_set_quantize is skipped.
The (int) casts on PNG_MAX__PALETTE_LENGTH are spurious and should be removed; it's already an (int) and, anyway, the arithmetic conversions would make the test fail correctly even if it were 256U (so was an (unsigned int); a negative (int) is converted into a larger unsigned value).
I didn't check whether the other test cases, the (-1) ones, cause crashes or errors but it seems reasonable to filter them out right at the start with an app error. I think both values are self evidently bogus.
A palette that is larger than maximum_colors is valid and is reduced to fit, so do not reject num_palette > PNG_MAX_PALETTE_LENGTH. Instead validate maximum_colors (positive and no more than the PNG palette maximum, which bounds the 8-bit quantized output) and reject non-positive counts. Use png_app_error for invalid counts so the application does not silently assume the palette mapping was set up. Size quantize_index for the input palette as well as the output: the pixel loop reads indices 0..255, but the reduction code indexes it with input palette entries, so with num_palette > PNG_MAX_PALETTE_LENGTH the previous fixed-size array could be accessed out of bounds. Rework the regression test to treat an oversized input palette as valid (reduction) and to require an application error for invalid counts.
|
Thanks for the detailed review — all points addressed in the latest commit (
Re-validated: |
|
I can't do the line-by-line reviews that I used to be able to do; I assume I've been locked out by the WC^3. So this is going to be difficult: The code looks wrong, intuitively wrong to me. Maybe I just died and went to heathen, but, Shirley, those three lines should just be "index_size = num_palette"? Ah, never mind; Wednesday. My power is out tomorrow. Whatever. |
Explain that quantize_index must cover both the output palette indices 0..255, read by the pixel loop in png_do_quantize (which can exceed num_palette for a malformed palette index), and the input palette indices read by the reduction code, so it is sized for the larger of num_palette and PNG_MAX_PALETTE_LENGTH.
|
Good question — it's not
Hence I built both variants and fed them a 1x1 palette PNG with PLTE=2 entries and a pixel index of 250, under a guard-page allocator:
I've also pushed a commit that documents this in the comment next to the sizing, so the "why not just num_palette" is self-explanatory in the code. |
That's a very good explanation. It doesn't fix the bug. I read your explanation and treated it at face value (sorry, I'm getting to used to thinking like everyone is an AI). What you say is correct (at face value) but the issue is that is not what the API documentation says and the bug cannot be in the API because that invalidates existing users of the documented API. The fix is not to change the API but, either, to document the API correctly (call an API with undocumented parameters and you own the bug) or to implement the API like it suggests. Bear in mind I'm just a dude here; the high and mighty are on vacation. Regardless of how much of the code I wrote everyone of us needs to put our own oxygen mask on first: I won't comment any more. I believe you are competent so you need to work out how to fix it. |
…alette Ensure both num_palette and maximum_colors are strictly bounded in [1, PNG_MAX_PALETTE_LENGTH] (256) as required by the PNG specification and libpng API contract. Revert quantize_index to fixed allocation size of PNG_MAX_PALETTE_LENGTH and update test cases accordingly.
|
Thanks @jbowler. Addressed by strictly enforcing the API bounds:
|
|
@jbowler
he is the core maintainer Not me
…On Fri, Aug 28, 2026 at 10:39 PM gamedeveloper8136 ***@***.***> wrote:
@gamedeveloper8136 <https://github.com/gamedeveloper8136> requested your
review on: pnggroup/libpng#907
<#907> png_set_quantize: validate
palette count parameters.
—
Reply to this email directly, view it on GitHub
<#907?email_source=notifications&email_token=B5NFMBYF6LW7HDMGD4VDHIT5MG4GHA5CNFSNUABQM5UWIORPF5TWS5BNNB2WEL2JONZXKZKFOZSW45CON52GSZTJMNQXI2LPNYXTGMBRG43TSMJRGE4TNJTSMVQXG33OWBZGK5TJMV3V64TFOF2WK43UMVSKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#event-30177911196>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/B5NFMBZPHF55UPYQSXV3QHD5MG4GHAVCNFSNUABDKJSXA33TNF2G64TZHMZTONRYGAZDKO2JONZXKZJ3GUYTEMRUGUZDCMRWUF3AE>
.
You are receiving this because your review was requested.Message ID:
***@***.***>
|
Summary
png_set_quantize()accepts an application-suppliednum_paletteandmaximum_colorsand uses them as allocation sizes, loop bounds, array indices, and amemcpylength — but validates neither. The internal arrays this function populates are sized for the PNG palette maximum, so invalid counts can produce out-of-bounds reads/writes and oversized copies.Problem
num_palettemust be in[1, PNG_MAX_PALETTE_LENGTH](256);maximum_colorsmust be positive.quantize_index[...](a 256-byte array),png_mallocsizes, loop bounds, andmemcpy(png_ptr->palette, palette, (unsigned int)num_palette * sizeof(png_color))into a fixed 768-byte owned buffer.num_palette = maximum_colorsis assigned at the end of the color-reduction path, so a non-positivemaximum_colorspropagates into the copy size.memcpy.Exploitability note: the trigger requires an application to call the public API with invalid arguments (for example a >256 app-computed palette count, a zero, or a negative/uninitialized value). It is not directly reachable from a crafted PNG file, because file-derived palette counts are already capped at 256 during PLTE parsing.
Fix
num_paletteandmaximum_colorsat entry.png_warning()+ early return, beforePNG_QUANTIZEis enabled, so no internal state is created for invalid input.Testing
pnggetsetregression test coveringnum_palette= 257, -1, 0,maximum_colors= -1, and a valid (4,4) call asserted to produce no warning.pngtest,pngvalid(standard + transform),pngunknown,pngimage, andpngcpsuites pass; UBSan clean; no new compiler warnings.Security context
The unsafe code paths became reachable through interactions between earlier security fixes (the
quantize_indexarray was fixed at 256 bytes by the CVE-2025-64505 fix, and the owned palette copy was added by the CVE-2026-33416 use-after-free fix); those fixes were correct for the defects they addressed, but the resulting code was not re-audited against out-of-range counts. This change is a hardening/validation fix consistent with the existing per-setter validation conventions.