Skip to content

fix(cjson): fix crashes and undefined behavior in generated code - #3557

Open
EloB wants to merge 2 commits into
glideapps:masterfrom
EloB:fix/cjson-generated-code
Open

EloB wants to merge 2 commits into
glideapps:masterfrom
EloB:fix/cjson-generated-code

Conversation

@EloB

@EloB EloB commented Nov 15, 2026

Copy link
Copy Markdown

Description

Five fixes in CJSONRenderer.ts, one for each problem in the issue:

  1. Top-level maps of integers, doubles, booleans or enums. The
    top-level getter now adds list and map elements the same way: it
    allocates each value, as the list already did
    (int64_t * item = cJSON_malloc(sizeof(*item)); ... hashtable_add(x->value, e->string, item, sizeof(*item));),
    and checks a map's values' types as it checks a list's. An element that
    cannot be allocated now fails the parse in lists and maps instead of
    being left out.
  2. Top-level lists and maps of null. The marker is now 0xDEADBEEF
    (it was 0xDEADBEAF). The top-level delete function deletes each
    element the way a class's list or map does: nothing for null,
    cJSON_Invalid, lists and maps, and a check for the marker around a
    nullable element.
  3. Enum getters. They now use if (cJSON_IsString(j)) instead of
    if (NULL != j), so strcmp never gets NULL. A value that is not a
    string reads as 0, as a string that is not a case already does.
  4. Patterns. regexec and regfree now run only if regcomp
    succeeded:
    regex_t regex; if (0 == regcomp(&regex, "…", REG_EXTENDED)) { if (regexec(…)) { regfree(&regex); …; return NULL; } regfree(&regex); }.
  5. Maps of classes and unions. The validated value is stored, instead
    of being deleted and parsed again.

A test commit adds two inputs: test/inputs/schema/top-level-integer-map.schema
with { "a": 1, "b": -2 }, and test/inputs/schema/top-level-null-array.schema
with [null, null]. They run for every language. Objective-C skips the
array of null, because its generated item check
([item isKindOfClass:id.class]) does not compile. That is a separate
Objective-C bug. PHP skips it with the other top-level array. Neither input
has an expected-failure sample. Java and Objective-C both accept
{"a": 1, "b": "two"} for a top-level map of integers, so such a sample
would need skips in other languages too. The cJSON map's type check was
checked by hand instead (see below).

Related Issue

Fixes #3552

Motivation and Context

Before this change, a top-level map of integers did not compile, and a
top-level list or map of null crashed when it was deleted. A number in
an array of enums crashed in strcmp. A pattern that regcomp cannot
compile led to regexec on an uninitialized regex_t. Maps of classes
did twice the work at every level of nesting.

Known limitations that remain:

  • JSON Schema patterns are ECMAScript regular expressions, and many
    (\d, lookahead) are not POSIX extended ones. Such a pattern is now not
    checked at all, where before it gave undefined behavior (on macOS,
    every value was rejected).
  • An enum value that is not a case (a non-string, or an unknown string)
    reads as 0, which lists, maps and unions store as if it were a case.
    Rejecting such values probably means changing the getter (an
    out-parameter or a sentinel) and is not part of this change.

Previous Behaviour / Output

./TopLevel.h:82:56: error: passing 'double' to parameter of incompatible type 'void *'
   82 |                     hashtable_add(x->value, e->string, cJSON_GetNumberValue(e), sizeof(int64_t *));
ERROR: AddressSanitizer: SEGV on unknown address 0x0000deadbe9f

([null, null], in cJSON_free from cJSON_DeleteTopLevel)

ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000
    #0 ... in strcmp

({"tags": [1]} for an array of enums)

New Behaviour / Output

All five inputs from the issue compile, run without AddressSanitizer or
UndefinedBehaviorSanitizer reports and print their input back.
{"tags": [1]} prints {"tags": []}, and {"a": "x"} for a top-level
map of integers is rejected (exit status 1).

The generated C changes in these places: the enum getters' if line,
every pattern check, maps of classes and unions, and the getters and
delete functions of top-level lists and maps. No other language's output
changes.

How Has This Been Tested?

Environment: macOS 26.6 (arm64), Node 24.16.0, Apple clang 21. The cjson
fixtures run their program under valgrind, which is not available on
macOS. A wrapper ran the program directly, so the fixtures' leak check
did not run here. The other languages ran with Python 3.12.16 and
mypy 2.4.1, OpenJDK 21.0.11 and Maven 3.10.0
(MAVEN_ARGS="-Dmaven.compiler.source=8 -Dmaven.compiler.target=8"),
Ruby 4.0.6, Swift, Rust (cargo) and Dart.

On this branch:

  • npm ci, npm run build, npm run lint (passes), npm run test:unit
    (54 files, 241 tests pass).
  • QUICKTEST=true FIXTURE=cjson,schema-cjson npm run test:fixtures: all
    138 runs pass.
  • QUICKTEST=true FIXTURE=cjson-default,cjson-multi-header,cjson-multi-split npm run test:fixtures:
    all 3 pass.
  • The two new inputs in the other fixtures that can run here:
    schema-typescript, schema-javascript, schema-typescript-zod,
    schema-typescript-effect-schema, schema-flow,
    schema-javascript-prop-types, schema-python, schema-ruby,
    schema-java, schema-java-datetime-legacy, schema-swift,
    schema-rust, schema-dart, schema-cplusplus and
    schema-objective-c: all pass (29 runs; Objective-C skips the array of
    null).
  • By hand, with test/fixtures/cjson/main.c and
    cc -g -O0 -fsanitize=address,undefined, before (master) and after:
    the five inputs from the issue, top-level maps of doubles, booleans and
    enums, and {"a": "x"} for the map of integers. Before, the maps do not
    compile, the lists and maps of null and the enum array crash, and the
    pattern rejects a valid value. After, all run without sanitizer reports,
    and the mistyped map value is rejected.

With only the test commit (the fix not applied):

  • QUICKTEST=true FIXTURE=schema-cjson npm run test:fixtures -- test/inputs/schema/top-level-integer-map.schema
    fails to compile (error: passing 'double' to parameter of incompatible type 'void *').
  • On top-level-null-array.schema, the program aborts (exit status 134).

Not run here, because the toolchains are not installed: C#, Go, Kotlin,
Scala, Elm, PHP, Elixir, Haskell, Crystal and Pike. The new inputs run in
their CI fixtures too.

Screenshots (if appropriate):

n/a


Written by Claude (an AI assistant made by Anthropic) and posted on behalf of the account owner (EloB). The reproduction, fix and tests were checked by running them as described above.

🤖 Generated with Claude Code

EloB and others added 2 commits November 15, 2026 08:12
`top-level-integer-map.schema` round-trips a map of integers, and
`top-level-null-array.schema` round-trips `[null, null]`. Neither has an
expected failure: several languages model `null` as an untyped value, and
several read a top-level map without checking its values' types.

PHP skips the array of null with the other top-level array, and
Objective-C skips it because the generated item check (`id.class`) does
not compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A top-level map of integers, booleans, doubles or enums stored the
  value itself as the pointer (which does not compile) and
  did not check the values' types. Allocate each value as the top-level
  list does, check the types as the list does, and fail the parse when an
  element of a top-level list or map cannot be allocated.
- Top-level lists of `null` marked elements with `0xDEADBEAF` (elsewhere
  `0xDEADBEEF`), and the delete functions of top-level lists and maps of
  `null` freed the marker. Use `0xDEADBEEF`, and delete each element as a
  class's list or map does.
- Enum getters passed `cJSON_GetStringValue(j)`, `NULL` for a value that
  is not a string, to `strcmp`. Check `cJSON_IsString(j)` instead of
  `NULL != j`; such a value reads as 0, as an unknown string does.
- Pattern checks ignored what `regcomp` returns, so `regexec` and
  `regfree` ran on a regex that did not compile. Check only patterns that
  compile; a JSON Schema pattern that is not a POSIX extended regular
  expression is not checked.
- Maps of classes and unions parsed every value twice, deleting the
  validated value. Store it instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: C (cJSON) output crashes or does not compile for top-level maps and lists, non-string enum values and some patterns

1 participant