Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
# We sort the list below via: sort -d -u
#
.DS_Store
/Makefile.local
/Makefile.orig
*.[ao]
/backup
/bug-report*
*build.log*
/c.tmpfile.*.c
.DS_Store
*.dSYM/
foobar
/jfmt
Expand Down Expand Up @@ -39,12 +37,12 @@ jparse_bug_report.*.c
/lex.jparse_.c
/.local.dir.tags
/makefile.local
/Makefile.local
/Makefile.orig
/.sorry.*.c
.*.sw[a-zA-Z0-9]*
.sw*
.*.sw[a-zA-Z0-9]*
/tags
/test_jparse/Makefile.local
/test_jparse/Makefile.orig
/test_jparse/c.tmpfile.*.c
/test_jparse/jnum_chk
/test_jparse/jnum_chk-*.tmp
Expand All @@ -53,10 +51,13 @@ jparse_bug_report.*.c
/test_jparse/jnum_test-*.tmp
/test_jparse/jparse_test.log
/test_jparse/.jparse_test.stderr.*
test_jparse/json_api_test
/test_jparse/jstr_test2.out
/test_jparse/jstr_test.out
/test_jparse/.local.dir.tags
/test_jparse/makefile.local
/test_jparse/Makefile.local
/test_jparse/Makefile.orig
/test_jparse/print_test
/test_jparse/pr_jparse_test
/test_jparse/pr_jparse_test-*.tmp
Expand Down
58 changes: 58 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,63 @@
# Significant changes in the JSON parser repo


## Release 2.6.0 2026-09-25

Performed a major code audit.

Harden parser allocation and length handling across JSON decode and scanner entry points

This PR audits the parser and supporting utilities for high-confidence
memory-safety and logic issues, with emphasis on allocation sizing, decode
paths, scanner length boundaries, and exact-length buffer handling. The
changes target overflow/OOB conditions in public and internal APIs while
preserving existing parser behavior for valid inputs.

- **JSON decode and encode bounds**
- Added checked `size_t` helpers for additive/multiplicative allocation math.
- Guarded encoded/decoded length accumulation before allocation and before buffer growth assumptions.
- Hardened decode buffer allocation against wrapped `mlen + 2` sizing.

- **Malformed `\u` escape handling**
- Changed `utf8len()` to return a signed length so malformed escapes propagate failure instead of wrapping to `SIZE_MAX`.
- Updated JSON decode paths to reject invalid UTF-8 escape length results before folding them into decoded-size calculations.

- **Scanner length boundary**
- Added an explicit `INT_MAX` guard before passing caller-controlled lengths into flex `yy_scan_bytes()`.
- Oversized inputs now fail safely instead of truncating through the scanner’s signed-length boundary.

- **Exact-length API correctness**
- Fixed `json_conv_bool()` and `json_conv_null()` to copy exactly `len` bytes and terminate locally.
- This removes the implicit read of `len + 1` bytes from caller-provided slices that are not required to be NUL-terminated.

- **Overflow hardening in utility paths**
- Applied checked allocation sizing to helper paths that duplicate or build derived strings
(`jstrencode`, `jstrdecode`, `jsemtblgen`, `verge`).
- Synced the scanner source change into the checked-in flex reference files used by non-regenerated builds.

- **Focused regression coverage**
- Added a small API-level regression test binary covering:
- malformed Unicode escapes
- exact-length boolean/null slices
- oversized `parse_json()` length rejection

Example of the exact-length fix:

```c
item->as_str = calloc(alloc_len, sizeof(*(item->as_str)));
memcpy(item->as_str, ptr, len);
item->as_str[len] = '\0';
```

In theory, the normal execution of this code should be identical to
"Release 2.5.11 2026-08-03", except in extreme or unusual cases.

Updated `JPARSE_REPO_VERSION` to `"2.6.0 2026-09-25"`.
Updated `JPARSE_TOOL_VERSION` to `"2.1.0 2026-09-25"`.
Updated `JPARSE_LIBRARY_VERSION` to `"2.5.0 2026-09-25"`.
Updated `JPARSE_UTILS_VERSION` to `"2.2.0 2026-06-25"`.


## Release 2.5.11 2026-08-03

Make jval.c `#include "jval.h"` rather than `jparse_main.h`. The code is
Expand Down
52 changes: 31 additions & 21 deletions jparse.l
Original file line number Diff line number Diff line change
Expand Up @@ -659,14 +659,24 @@ parse_json(char const *ptr, size_t len, char const *filename, bool *is_valid)
tree = json_alloc(JTYPE_UNSET);
return tree;
}
if (len > (size_t)(INT_MAX - 2)) {
werr(40, __func__, "len: %zu > INT_MAX - 2: %d", len, INT_MAX - 2);

/*
* yy_scan_bytes() accepts an int length, so oversized input cannot be scanned safely.
*/
*is_valid = false;
tree = json_alloc(JTYPE_UNSET);
return tree;
}

/*
* initialise scanner
*/
errno = 0;
ret = jparse_lex_init_extra(&extra, &scanner);
if (ret != 0) {
werrp(40, __func__, "jparse_lex_init_extra failed");
werrp(41, __func__, "jparse_lex_init_extra failed");

/*
* if jparse_lex_init_extra() reports an error (!= 0) then *is_valid
Expand All @@ -687,7 +697,7 @@ parse_json(char const *ptr, size_t len, char const *filename, bool *is_valid)
* perhaps it should call err() instead but for now we make it a
* non-fatal error as well.
*/
werr(41, __func__, "unable to scan string");
werr(42, __func__, "unable to scan string");

/*
* since we cannot scan the bytes, we set *is_valid to false, even
Expand Down Expand Up @@ -829,7 +839,7 @@ parse_json_str(char const *ptr, size_t len, bool *is_valid)
* firewall
*/
if (is_valid == NULL) {
err(42, __func__, "is_valid == NULL");
err(43, __func__, "is_valid == NULL");
not_reached();
} else {
/*
Expand All @@ -844,7 +854,7 @@ parse_json_str(char const *ptr, size_t len, bool *is_valid)
* firewall
*/
if (ptr == NULL) {
werr(43, __func__, "ptr is NULL");
werr(44, __func__, "ptr is NULL");

/*
* flag that we have invalid JSON
Expand Down Expand Up @@ -909,7 +919,7 @@ parse_json_stream(FILE *stream, char const *filename, bool *is_valid)
* firewall
*/
if (is_valid == NULL) {
err(44, __func__, "is_valid == NULL");
err(45, __func__, "is_valid == NULL");
not_reached();
} else {
/*
Expand All @@ -933,7 +943,7 @@ parse_json_stream(FILE *stream, char const *filename, bool *is_valid)
/*
* report NULL stream
*/
werr(45, __func__, "stream is NULL");
werr(46, __func__, "stream is NULL");

/*
* flag that we have invalid JSON
Expand All @@ -955,7 +965,7 @@ parse_json_stream(FILE *stream, char const *filename, bool *is_valid)
/*
* report closed stream
*/
werr(46, __func__, "stream is not open");
werr(47, __func__, "stream is not open");

/*
* flag that we have invalid JSON
Expand All @@ -978,7 +988,7 @@ parse_json_stream(FILE *stream, char const *filename, bool *is_valid)
/*
* warn about read error
*/
werr(47, __func__, "could not read read stream");
werr(48, __func__, "could not read read stream");
/*
* we need to clearerr() or fclose(), depending on the stream.
*/
Expand All @@ -1004,14 +1014,14 @@ parse_json_stream(FILE *stream, char const *filename, bool *is_valid)
* report invalid bytes
*/
if (low_bytes > 0 && nul_bytes > 0) {
werr(48, __func__, "%zu low byte%s and %zu NUL byte%s detected: data block is NOT valid JSON",
werr(49, __func__, "%zu low byte%s and %zu NUL byte%s detected: data block is NOT valid JSON",
low_bytes, low_bytes > 1 ? "s":"",
nul_bytes, nul_bytes > 1 ? "s":"");
} else if (low_bytes > 0) {
werr(49, __func__, "%zu low byte%s detected: data block is NOT valid JSON",
werr(50, __func__, "%zu low byte%s detected: data block is NOT valid JSON",
low_bytes, low_bytes > 1 ? "s":"");
} else if (nul_bytes > 0) {
werr(50, __func__, "%zu NUL byte%s detected: data block is NOT valid JSON",
werr(51, __func__, "%zu NUL byte%s detected: data block is NOT valid JSON",
nul_bytes, nul_bytes > 1 ? "s":"");
}

Expand Down Expand Up @@ -1092,7 +1102,7 @@ exists(char const *path)
* firewall
*/
if (path == NULL) {
err(51, __func__, "called with NULL path");
err(52, __func__, "called with NULL path");
not_reached();
}

Expand Down Expand Up @@ -1133,7 +1143,7 @@ is_file(char const *path)
* firewall
*/
if (path == NULL) {
err(52, __func__, "called with NULL path");
err(53, __func__, "called with NULL path");
not_reached();
}

Expand Down Expand Up @@ -1184,7 +1194,7 @@ is_read(char const *path)
* firewall
*/
if (path == NULL) {
err(53, __func__, "called with NULL path");
err(54, __func__, "called with NULL path");
not_reached();
}

Expand Down Expand Up @@ -1244,7 +1254,7 @@ parse_json_file(char const *filename, bool *is_valid)
* firewall
*/
if (is_valid == NULL) {
err(54, __func__, "is_valid == NULL");
err(55, __func__, "is_valid == NULL");
not_reached();
} else {
/*
Expand All @@ -1255,7 +1265,7 @@ parse_json_file(char const *filename, bool *is_valid)
*is_valid = true;
}
if (filename == NULL) {
werr(55, __func__, "passed NULL filename");
werr(56, __func__, "passed NULL filename");

/*
* flag that we have invalid JSON
Expand All @@ -1273,7 +1283,7 @@ parse_json_file(char const *filename, bool *is_valid)
/*
* warn about bogus filename
*/
werr(56, __func__, "passed empty filename");
werr(57, __func__, "passed empty filename");

/*
* flag that we have invalid JSON
Expand Down Expand Up @@ -1305,7 +1315,7 @@ parse_json_file(char const *filename, bool *is_valid)
/*
* report missing file
*/
werr(57, __func__, "passed filename that's not actually a file: %s", filename);
werr(58, __func__, "passed filename that's not actually a file: %s", filename);

/*
* flag that we have invalid JSON
Expand All @@ -1323,7 +1333,7 @@ parse_json_file(char const *filename, bool *is_valid)
/*
* report that file is not a normal file
*/
werr(58, __func__, "passed filename not a normal file: %s", filename);
werr(59, __func__, "passed filename not a normal file: %s", filename);

/*
* report invalid JSON
Expand All @@ -1340,7 +1350,7 @@ parse_json_file(char const *filename, bool *is_valid)
/*
* report unreadable file
*/
werr(59, __func__, "passed filename not a readable file: %s", filename);
werr(60, __func__, "passed filename not a readable file: %s", filename);

/*
* flag that we have invalid JSON
Expand All @@ -1364,7 +1374,7 @@ parse_json_file(char const *filename, bool *is_valid)
/*
* warn about file open error
*/
werrp(60, __func__, "couldn't open file %s, ignoring", filename);
werrp(61, __func__, "couldn't open file %s, ignoring", filename);

/*
* flag that we have invalid JSON
Expand Down
Loading
Loading