json-c uses duplocale+newlocale+uselocale() to temporarily switch LC_NUMERIC
to "C" during JSON number parsing.
On AIX, every call to json_tokener_parse_ex permanently leaks memory.
This was confirmed with dbx malloc allocation.
0x20008af8 24 0 YORKTOWN
0x20008e18 5128 0 YORKTOWN
0x2000a458 776 0 YORKTOWN
0x2000a768 776 0 YORKTOWN
0x2000aa78 776 0 YORKTOWN
0x2000ad88 776 0 YORKTOWN
0x2000b098 776 0 YORKTOWN
0x2000b6b8 776 0 YORKTOWN
dbx malloc allocation showing new allocations surviving after each parse call with ~776
bytes left after invocations.
The root cause is that on AIX, locale category objects allocated by duplocale() are not
released back to the process heap, even when freelocale() is called correctly.
This causes unbounded heap growth in any long-running process that parses JSON repeatedly.
The fix it to replace the per-call duplocale+newlocale+reelocale() with a single
file-scope static locale_t created once on first call using
newlocale(LC_NUMERIC_MASK, "C", (locale_t)0) and reused for the lifetime of the library.
This reduces newlocale calls on all platforms.
__attribute__((destructor)) function json_tokener_locale_fini() frees the static locale
when the library is unloaded via dlclose() or process exit.
json_patch_apply_move_copy() checks that "from" is not a proper prefix
of "path" per RFC 6902 section 4.4 (a location cannot be moved into one
of its own children), but it did so with a plain strncmp() over the raw
pointer strings, without checking that the shared run of characters
actually ends on a JSON Pointer reference-token boundary in "path".
As a result, moving or copying between two sibling object keys where
one name is a textual prefix of the other, e.g. "/aa" to "/aab", was
rejected with "Invalid attempt to move parent under a child" even
though "/aa" is not an ancestor pointer of "/aab" at all, just an
unrelated key that happens to share a few leading characters.
The fix requires that the character in "path" right after the shared
"from" prefix be either the end of the string (equal paths, a no-op)
or a '/' (a genuine nested reference token), before treating it as an
attempt to move a location under its own descendant.
Added two cases to json_patch_tests.json covering the move and copy
operations between such sibling keys, and refreshed the corresponding
golden test_json_patch.expected output.
Some test_json_patch test cases have neither a comment field and
nor a error field, so the expression
"comment ? comment : error_s" evaluates to NULL.
The printf statement
printf("Testing '%s', ...", comment ? comment : error_s, ...);
passes NULL as a %s argument. GNU libc handles this gracefully and prints (null).
On AIX, passing NULL for %s prints an empty string, causing the test output
to differ from the expected file.
The fix adds a simple macro:
define STR_OR_NULL(s) ((s) ? (s) : "(null)")
that retruns "(null)" when s is NULL.
still waiting for more data, and also after read() failures.
Finalize the tokener at EOF with the terminating NUL so valid scalar
values can complete, while rejecting genuinely truncated input.
Also propagate read errors to the caller and add CLI regression tests.
json_tokener.c assigned to an int64_t through a (uint64_t) cast. The
guarded range makes it harmless, but the cast names the wrong type.
array_list_expand_internal() reallocated when max == arr->size even though
the array can already hold max entries at that point, since callers only
index up to max - 1. Skip the realloc.
lseek() expects (fd, offset, whence). The test passed (fd, SEEK_SET, 0),
which does not conform to the function signature. It should be corrected
to (fd, 0, SEEK_SET).
Keep Amiga-specific files under amiga/, cache the pinned LHa binary in
CI, and make it explicit that the workflow artifact is not an official
release.
Co-authored-by: Cursor <cursoragent@cursor.com>
Strict mode rejected leading zeros but nothing else about the shape of a
number, so several malformed numbers parsed and were then written back out
verbatim, producing JSON that other parsers reject:
$ echo '[2.e3]' | ./json_parse -s -
[ 2.e3 ]
RFC 8259 requires a mandatory integer part and at least one digit in both the
fraction and the exponent:
number = [ minus ] int [ frac ] [ exp ]
int = zero / ( digit1-9 *DIGIT )
frac = decimal-point 1*DIGIT
exp = e [ minus / plus ] 1*DIGIT
so "1.", "-2.", "2.e3", "2.e+3", "0.e1", "-.123", "1e" and "1e+" are all
invalid. Walk the accumulated text against that grammar instead. The leading
zero rule from the previous check is part of the same walk rather than a
separate test, and its cases keep their coverage.
Only JSON_TOKENER_STRICT is affected; the default tokener stays as lenient as
it was. Measured against JSONTestSuite (318 files): strict mode went from 44
to 37 files accepted that the suite says must be rejected, default mode stayed
at 71, and nothing that must be accepted regressed in either mode.