Commit f3bf03e
committed
libretro-common/formats/json/rjson: integer overflow hardening + test
Audit of rjson.c (1569 lines; the JSON parser + writer used by
configs, manifests, RetroAchievements, cloud sync, etc.). Six
integer-overflow/underflow hardenings in the parser and writer,
plus a regression test. None are remotely exploitable in the
typical usage pattern -- they require inputs or outputs
approaching or exceeding 2 GiB -- but each is a real correctness
bug that can bite on 32-bit targets or on pathological
locally-generated JSON.
rjson: cap _rjson_grow_string doubling
The growing string buffer doubled its capacity unconditionally:
size_t new_string_cap = json->string_cap * 2;
Once string_cap exceeds SIZE_MAX / 2, the doubling wraps to 0 or
a tiny value. realloc() with that argument is implementation-
defined: glibc returns NULL (benign, caller errors out); some
other libcs return a valid 0-byte pointer. The following
pushchar then writes at string[0], which is past the newly
shrunk allocation. Heap overflow.
Reachable on 32-bit with a ~2 GiB JSON string token (and
correspondingly much harder on 64-bit). Fix: cap
new_string_cap at _RJSON_MAX_SIZE (256 MiB) and fail cleanly
when the growth would exceed it.
rjson: clamp rjson_open_user io_block_size
The public entry point accepted any int as the input block size
and allocated sizeof(rjson_t) - sizeof(input_buf) + io_block_size.
A small io_block_size (including 0 or negative) underallocated
input_buf to fewer bytes than the fixed struct expected, and
the first read in _rjson_io_input ran off the end.
The two internal callers (rjson_open_stream, rjson_open_rfile)
already bound io_block_size to [1024, 4096], so this isn't
reachable via them. It is reachable from any consumer that
calls rjson_open_user directly with attacker-influenced sizing,
so clamp it here for defense-in-depth: io_block_size floored at
16, ceilinged at _RJSON_MAX_SIZE.
rjsonwriter: size_t arithmetic in _rjsonwriter_memory_io
new_cap = writer->buf_num + (is_append ? len : 0) + 512;
All operands are signed int. For a writer generating >2 GiB of
output (local database dumps, long JSON-encoded state blobs)
the sum overflows INT_MAX (UB). Post-overflow the comparison
"new_cap > buf_cap" misbehaves, the realloc is skipped, and the
subsequent memcpy in the same function writes past the existing
allocation.
Fix: redo the math in size_t, overflow-check against
_RJSON_MAX_SIZE, set error_text and return 0 on failure. The
realloc call then receives a sane positive int.
rjsonwriter: size_t arithmetic in rjsonwriter_raw
Same class of bug:
if (writer->buf_num + len > writer->buf_cap)
rjsonwriter_flush(writer);
Signed-int sum overflows for large writes and the flush is
skipped, letting the downstream memcpy blow past the buffer.
Also: nothing guarded against a negative len. A caller passing
len < 0 pre-patch advanced buf_num by a negative amount via a
later "buf_num += len" and the next write corrupted the ring.
Fix: reject negative len at the top, redo the bounds comparison
in size_t.
rjsonwriter: size_t arithmetic in rjsonwriter_rawf
Same class. newcap = buf_num + need + 1 could overflow for a
single oversized formatted value. Same fix pattern.
Regression test: libretro-common/samples/formats/json/rjson_test.c
Six subtests, three of which are true discriminators:
1. parser happy path (nested object + array)
2. long-string parse (forces _rjson_grow_string growth past
the inline buffer) -- smoke for patch #1
3. malformed inputs: 12 variants (truncated objects, dangling
surrogates, bad numbers, ...) must NOT crash. Smoke for
general robustness.
4. writer happy path
5. rjsonwriter_raw with len = -99 -- DIRECT DISCRIMINATOR for
the negative-len fix. Pre-patch under ASan this fires:
AddressSanitizer: negative-size-param: (size=-99)
in memcpy at rjson.c:1392 (rjsonwriter_raw)
Post-patch the call is a no-op and the output is exactly
the expected 6-byte "AAABBB".
6. rjson_open_user with tiny io_block_size (-10..8) -- DIRECT
DISCRIMINATOR for the floor fix. Pre-patch
io_block_size=0 allocated zero bytes for input_buf and the
first read was OOB (UB at C level, crashes on some libcs).
Post-patch all values floor to 16 and the open+read+free
cycle is clean.
Built and ran under -Wall -pedantic -std=gnu99:
All 6 subtests pass on patched code.
ASan -O0 (patched): clean, exit 0.
ASan -O0 (unpatched, for the negative-len subtest):
"negative-size-param: (size=-99)" in memcpy -> immediate abort.
CI: libretro-common samples workflow updated. rjson_test added
to RUN_TARGETS. Full local dry-run under the GHA shell contract:
Built: 15 Ran: 15 Failed: 0
VC6 compatibility: rjson.c is compiled on all platforms. The
fix uses only size_t / ssize_t / int and the existing stdint
shim; <limits.h> (C89) is now explicitly included for INT_MAX.
_RJSON_MAX_SIZE is a size_t-cast literal, valid in C89.1 parent eb86df0 commit f3bf03e
4 files changed
Lines changed: 401 additions & 5 deletions
File tree
- .github/workflows
- libretro-common
- formats/json
- samples/formats/json
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
| 58 | + | |
58 | 59 | | |
59 | 60 | | |
60 | 61 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
| 28 | + | |
29 | 29 | | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
30 | 41 | | |
31 | 42 | | |
32 | 43 | | |
| |||
139 | 150 | | |
140 | 151 | | |
141 | 152 | | |
142 | | - | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
143 | 170 | | |
144 | 171 | | |
145 | 172 | | |
| |||
915 | 942 | | |
916 | 943 | | |
917 | 944 | | |
918 | | - | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
| 952 | + | |
| 953 | + | |
| 954 | + | |
919 | 955 | | |
920 | 956 | | |
921 | 957 | | |
| |||
1290 | 1326 | | |
1291 | 1327 | | |
1292 | 1328 | | |
1293 | | - | |
| 1329 | + | |
| 1330 | + | |
| 1331 | + | |
| 1332 | + | |
| 1333 | + | |
| 1334 | + | |
| 1335 | + | |
| 1336 | + | |
| 1337 | + | |
| 1338 | + | |
| 1339 | + | |
| 1340 | + | |
| 1341 | + | |
| 1342 | + | |
| 1343 | + | |
| 1344 | + | |
| 1345 | + | |
| 1346 | + | |
1294 | 1347 | | |
1295 | 1348 | | |
1296 | 1349 | | |
| |||
1376 | 1429 | | |
1377 | 1430 | | |
1378 | 1431 | | |
1379 | | - | |
| 1432 | + | |
| 1433 | + | |
| 1434 | + | |
| 1435 | + | |
| 1436 | + | |
| 1437 | + | |
| 1438 | + | |
1380 | 1439 | | |
1381 | 1440 | | |
1382 | 1441 | | |
| |||
1424 | 1483 | | |
1425 | 1484 | | |
1426 | 1485 | | |
| 1486 | + | |
| 1487 | + | |
| 1488 | + | |
| 1489 | + | |
| 1490 | + | |
| 1491 | + | |
| 1492 | + | |
| 1493 | + | |
1427 | 1494 | | |
1428 | 1495 | | |
1429 | 1496 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
0 commit comments