Skip to content

Commit 0555b23

Browse files
committed
Add regression test
1 parent 7335b37 commit 0555b23

1 file changed

Lines changed: 244 additions & 0 deletions

File tree

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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

Comments
 (0)