|
| 1 | +/* Copyright (C) 2026 The RetroArch team |
| 2 | + * |
| 3 | + * --------------------------------------------------------------------------------------- |
| 4 | + * The following license statement only applies to this file (test_rpng.c). |
| 5 | + * --------------------------------------------------------------------------------------- |
| 6 | + * |
| 7 | + * Permission is hereby granted, free of charge, |
| 8 | + * to any person obtaining a copy of this software and associated documentation files (the "Software"), |
| 9 | + * to deal in the Software without restriction, including without limitation the rights to |
| 10 | + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, |
| 11 | + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
| 12 | + * |
| 13 | + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. |
| 14 | + * |
| 15 | + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
| 16 | + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 17 | + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
| 18 | + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
| 19 | + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 20 | + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 21 | + */ |
| 22 | + |
| 23 | +/* Regression coverage for rpng_process_ihdr's dimension and |
| 24 | + * size guards. |
| 25 | + * |
| 26 | + * The picture is two-layer: |
| 27 | + * |
| 28 | + * - On all hosts a 4 GiB output guard (width*height*4) plus a |
| 29 | + * 4 GiB pass_size guard reject images whose decoded buffer |
| 30 | + * cannot be addressed. Together with the (size_t) casts at |
| 31 | + * the per-row malloc sites these prevent the original heap |
| 32 | + * overflow on any platform regardless of dimensions. |
| 33 | + * |
| 34 | + * - On 32-bit hosts an additional 0x4000 (16384) dimension cap |
| 35 | + * rejects images that would demand more than a few hundred MB |
| 36 | + * of decoded pixels. These would fail to allocate anyway on |
| 37 | + * a 32-bit address space, but a tight cap turns the failure |
| 38 | + * into a clean reject rather than a partially-set-up parser |
| 39 | + * state. 64-bit hosts do not cap here, allowing legitimate |
| 40 | + * large images (cf. IrfanView's tens-of-thousands-pixel |
| 41 | + * routine support). |
| 42 | + * |
| 43 | + * Tests below are platform-gated to match. The strict |
| 44 | + * regression cases (the 0x4001 / 30000-squared bug shapes) only |
| 45 | + * fire on 32-bit; 64-bit gets the looser sanity coverage. */ |
| 46 | + |
| 47 | +#include <check.h> |
| 48 | +#include <stdint.h> |
| 49 | +#include <stdlib.h> |
| 50 | +#include <string.h> |
| 51 | + |
| 52 | +#include <formats/rpng.h> |
| 53 | + |
| 54 | +#define SUITE_NAME "rpng" |
| 55 | + |
| 56 | +/* PNG file signature, replicated from rpng_internal.h (which is |
| 57 | + * not part of the public install set). */ |
| 58 | +static const uint8_t png_magic[8] = { |
| 59 | + 0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a, |
| 60 | +}; |
| 61 | + |
| 62 | +/* Build a minimal valid-shape PNG buffer containing the file |
| 63 | + * signature and a single IHDR chunk with the supplied dimensions, |
| 64 | + * followed by a trailing-padding chunk-header so that |
| 65 | + * rpng_iterate_image's post-IHDR pointer-advance check does not |
| 66 | + * push past buff_end on the same call (a successful IHDR-accept |
| 67 | + * call must leave buff_data <= buff_end so a subsequent iterate |
| 68 | + * could read the next chunk). The CRC is set to zero - rpng's |
| 69 | + * iterate path does not validate IHDR CRC, so this is sufficient |
| 70 | + * to exercise rpng_process_ihdr. */ |
| 71 | +static size_t make_ihdr_only_png(uint8_t *out, size_t out_size, |
| 72 | + uint32_t width, uint32_t height, |
| 73 | + uint8_t depth, uint8_t color_type) |
| 74 | +{ |
| 75 | + /* 8 (magic) + 4 (length) + 4 (type) + 13 (IHDR data) + 4 (CRC) |
| 76 | + * + 8 (room for next chunk header) = 41 */ |
| 77 | + size_t len = 0; |
| 78 | + if (out_size < 41) |
| 79 | + return 0; |
| 80 | + |
| 81 | + memcpy(out + len, png_magic, 8); |
| 82 | + len += 8; |
| 83 | + |
| 84 | + /* IHDR chunk length = 13, big-endian */ |
| 85 | + out[len++] = 0; out[len++] = 0; out[len++] = 0; out[len++] = 13; |
| 86 | + |
| 87 | + /* "IHDR" */ |
| 88 | + out[len++] = 'I'; out[len++] = 'H'; out[len++] = 'D'; out[len++] = 'R'; |
| 89 | + |
| 90 | + /* width, big-endian */ |
| 91 | + out[len++] = (uint8_t)(width >> 24); |
| 92 | + out[len++] = (uint8_t)(width >> 16); |
| 93 | + out[len++] = (uint8_t)(width >> 8); |
| 94 | + out[len++] = (uint8_t)(width >> 0); |
| 95 | + |
| 96 | + /* height, big-endian */ |
| 97 | + out[len++] = (uint8_t)(height >> 24); |
| 98 | + out[len++] = (uint8_t)(height >> 16); |
| 99 | + out[len++] = (uint8_t)(height >> 8); |
| 100 | + out[len++] = (uint8_t)(height >> 0); |
| 101 | + |
| 102 | + out[len++] = depth; |
| 103 | + out[len++] = color_type; |
| 104 | + out[len++] = 0; /* compression */ |
| 105 | + out[len++] = 0; /* filter */ |
| 106 | + out[len++] = 0; /* interlace */ |
| 107 | + |
| 108 | + /* CRC placeholder; rpng_iterate_image does not validate it */ |
| 109 | + out[len++] = 0; out[len++] = 0; out[len++] = 0; out[len++] = 0; |
| 110 | + |
| 111 | + /* Trailing 8 bytes so the post-IHDR pointer advance leaves |
| 112 | + * buff_data <= buff_end (rpng_iterate_image returns false if |
| 113 | + * the advance pushes past the end). Contents do not matter - |
| 114 | + * the test does not call rpng_iterate_image again. */ |
| 115 | + out[len++] = 0; out[len++] = 0; out[len++] = 0; out[len++] = 0; |
| 116 | + out[len++] = 0; out[len++] = 0; out[len++] = 0; out[len++] = 0; |
| 117 | + |
| 118 | + return len; |
| 119 | +} |
| 120 | + |
| 121 | +/* Helper: try to parse an IHDR-only PNG with the supplied |
| 122 | + * dimensions and depth/color_type, returning the result of |
| 123 | + * rpng_iterate_image. */ |
| 124 | +static bool try_iterate(uint32_t w, uint32_t h, uint8_t depth, uint8_t ctype) |
| 125 | +{ |
| 126 | + uint8_t buf[64]; |
| 127 | + size_t len; |
| 128 | + rpng_t *rpng; |
| 129 | + bool ret; |
| 130 | + |
| 131 | + len = make_ihdr_only_png(buf, sizeof(buf), w, h, depth, ctype); |
| 132 | + ck_assert(len > 0); |
| 133 | + |
| 134 | + rpng = rpng_alloc(); |
| 135 | + ck_assert(rpng != NULL); |
| 136 | + ck_assert(rpng_set_buf_ptr(rpng, buf, len)); |
| 137 | + ck_assert(rpng_start(rpng)); |
| 138 | + |
| 139 | + ret = rpng_iterate_image(rpng); |
| 140 | + |
| 141 | + rpng_free(rpng); |
| 142 | + return ret; |
| 143 | +} |
| 144 | + |
| 145 | +START_TEST (test_rpng_ihdr_dimension_cap_accept_at_limit) |
| 146 | +{ |
| 147 | + /* 0x4000 == 16384. Inclusive accept on every platform: on |
| 148 | + * 32-bit this is the boundary of the dimension cap; on 64-bit |
| 149 | + * there is no dimension cap and 16384x16384 RGBA8 is well |
| 150 | + * under the 4 GiB output guard. */ |
| 151 | + ck_assert(try_iterate(0x4000u, 0x4000u, 8, 6)); |
| 152 | +} |
| 153 | +END_TEST |
| 154 | + |
| 155 | +#if SIZE_MAX <= 0xFFFFFFFFu |
| 156 | +START_TEST (test_rpng_ihdr_dimension_cap_reject_just_over_32bit) |
| 157 | +{ |
| 158 | + /* 0x4001 must be rejected on 32-bit. The pre-existing 4 GiB |
| 159 | + * output guard does not catch 16385x16385 RGBA8 (~1.07 GiB), |
| 160 | + * so the 0x4000 cap is what rejects it here. This case does |
| 161 | + * NOT reproduce on 64-bit, where 16385x16385 is a legitimate |
| 162 | + * (large) image. */ |
| 163 | + ck_assert(!try_iterate(0x4001u, 0x4000u, 8, 6)); |
| 164 | + ck_assert(!try_iterate(0x4000u, 0x4001u, 8, 6)); |
| 165 | + ck_assert(!try_iterate(0x4001u, 0x4001u, 8, 6)); |
| 166 | +} |
| 167 | +END_TEST |
| 168 | + |
| 169 | +START_TEST (test_rpng_ihdr_dimension_cap_reject_30000_squared_32bit) |
| 170 | +{ |
| 171 | + /* 30000x30000 RGBA8 is the historical worst case on 32-bit: |
| 172 | + * 3.35 GiB of decoded pixels, which on a 32-bit address space |
| 173 | + * cannot be allocated and pre-patch corrupted the heap when |
| 174 | + * the uint32 multiplication width*height*sizeof(uint32_t) |
| 175 | + * wrapped. The 0x4000 cap catches this. On 64-bit this is a |
| 176 | + * legitimate-but-large image, accepted by the IHDR guards. */ |
| 177 | + ck_assert(!try_iterate(30000u, 30000u, 8, 6)); |
| 178 | +} |
| 179 | +END_TEST |
| 180 | +#endif |
| 181 | + |
| 182 | +START_TEST (test_rpng_ihdr_size_cap_reject_uint32_max) |
| 183 | +{ |
| 184 | + /* PNG-spec maximum dimensions. Rejected on every platform: |
| 185 | + * on 32-bit the 0x4000 cap catches it first; on 64-bit the |
| 186 | + * 4 GiB output guard does (the math overflows even with |
| 187 | + * 64-bit width arithmetic). */ |
| 188 | + ck_assert(!try_iterate(0x7FFFFFFFu, 0x7FFFFFFFu, 8, 6)); |
| 189 | + ck_assert(!try_iterate(0x7FFFFFFFu, 1u, 8, 6)); |
| 190 | + ck_assert(!try_iterate(1u, 0x7FFFFFFFu, 8, 6)); |
| 191 | +} |
| 192 | +END_TEST |
| 193 | + |
| 194 | +START_TEST (test_rpng_ihdr_dimension_cap_accept_small) |
| 195 | +{ |
| 196 | + /* Sanity: small valid dimensions still parse on every |
| 197 | + * platform. */ |
| 198 | + ck_assert(try_iterate(16u, 16u, 8, 6)); |
| 199 | + ck_assert(try_iterate(1u, 1u, 8, 6)); |
| 200 | + /* Other supported color/depth combinations at the 0x4000 |
| 201 | + * boundary. 16384x16384 RGBA-16 is 2 GiB output -- under the |
| 202 | + * 4 GiB cap on every platform. */ |
| 203 | + ck_assert(try_iterate(0x4000u, 0x4000u, 8, 2)); /* RGB */ |
| 204 | + ck_assert(try_iterate(0x4000u, 0x4000u, 16, 6)); /* RGBA-16 */ |
| 205 | +} |
| 206 | +END_TEST |
| 207 | + |
| 208 | +START_TEST (test_rpng_ihdr_zero_dimensions_rejected) |
| 209 | +{ |
| 210 | + /* Pre-existing behavior: zero dimensions are rejected. |
| 211 | + * Verify the cap patch did not regress this. */ |
| 212 | + ck_assert(!try_iterate(0u, 16u, 8, 6)); |
| 213 | + ck_assert(!try_iterate(16u, 0u, 8, 6)); |
| 214 | +} |
| 215 | +END_TEST |
| 216 | + |
| 217 | +Suite *create_suite(void) |
| 218 | +{ |
| 219 | + Suite *s = suite_create(SUITE_NAME); |
| 220 | + |
| 221 | + TCase *tc_core = tcase_create("Core"); |
| 222 | + tcase_add_test(tc_core, test_rpng_ihdr_dimension_cap_accept_at_limit); |
| 223 | +#if SIZE_MAX <= 0xFFFFFFFFu |
| 224 | + tcase_add_test(tc_core, test_rpng_ihdr_dimension_cap_reject_just_over_32bit); |
| 225 | + tcase_add_test(tc_core, test_rpng_ihdr_dimension_cap_reject_30000_squared_32bit); |
| 226 | +#endif |
| 227 | + tcase_add_test(tc_core, test_rpng_ihdr_size_cap_reject_uint32_max); |
| 228 | + tcase_add_test(tc_core, test_rpng_ihdr_dimension_cap_accept_small); |
| 229 | + tcase_add_test(tc_core, test_rpng_ihdr_zero_dimensions_rejected); |
| 230 | + suite_add_tcase(s, tc_core); |
| 231 | + |
| 232 | + return s; |
| 233 | +} |
| 234 | + |
| 235 | +int main(void) |
| 236 | +{ |
| 237 | + int num_fail; |
| 238 | + Suite *s = create_suite(); |
| 239 | + SRunner *sr = srunner_create(s); |
| 240 | + srunner_run_all(sr, CK_NORMAL); |
| 241 | + num_fail = srunner_ntests_failed(sr); |
| 242 | + srunner_free(sr); |
| 243 | + return (num_fail == 0) ? EXIT_SUCCESS : EXIT_FAILURE; |
| 244 | +} |
0 commit comments