Skip to content

Gravity v0.9.8

Latest

Choose a tag to compare

@marcobambini marcobambini released this 05 Aug 13:56

Security and memory-safety release. Most of the crashes below were reported by external researchers fuzzing the compiler and the bytecode loader, and each fix ships with a regression test.

Fixed

Bytecode loader (gravity -x / gravity_vm_loadbuffer)

These are reachable from attacker-controlled serialized bytecode, so they matter to any embedder that loads a .g file it did not produce itself.

  • NULL dereference in gravity_vm_loadbuffer — a function object without an identifier field, such as {"x":{"type":"function"}}, reached strlen(NULL) and killed the process. The loader now validates the shape of a JSON executable before trusting it: the root and every entry must be objects, the identifier must appear exactly once and be a string, and unknown object types are rejected. Malformed input becomes a load error instead of a crash (#444)
  • Signed 64-bit integer overflow in json_parse_ex — the integer and exponent accumulators multiplied by 10 per digit with no range check, so any literal past 19 significant digits overflowed. Signed overflow is undefined in C: the parser stored a wrapped value, and -fsanitize=undefined builds trapped with SIGILL. Both accumulators are range-checked now (#447)
  • Pointer-arithmetic overflow in the JSON scan loop — the scanner incremented its cursor unconditionally, so input ending while still inside a string or comment advanced the pointer past one-past-the-end, which is undefined behaviour. The loop now stops at the end of the buffer whatever state the scanner is in (#448)

Compiler

  • Heap out-of-bounds read in parse_number_expression — the 0x/0b/0o prefix check read value[1] without confirming the token had two bytes, so a file whose last token was a bare 0 read one byte past the buffer (#446)
  • Crash (SIGFPE) folding a floating-point remainder — the optimizer folded % by truncating both operands to int64_t, so any divisor with 0 < |divisor| < 1 became an integer division by zero and killed the compiler on input as small as 1 % 0.5. Float remainder is now folded with remainder(), matching the runtime, and mixed Int/Float remainders are left to the VM because REM dispatches on the class of the left operand. This also corrects a silent wrong answer: 5.5 % 2.0 folded to 1 where the VM evaluates -0.5 (#443)
  • Undefined behaviour in Int arithmetic — Gravity Ints wrap on overflow, but the wrap was done on signed operands in the VM fast path, in the operator_int_* methods and in the constant folder. New GRAVITY_INT_ADD/SUB/MUL/NEG/DIV/REM helpers do the arithmetic on the unsigned counterpart, so the values are unchanged but no longer undefined. They also cover GRAVITY_INT_MIN op -1, which on x86 faults in idiv rather than merely wrapping (#443)
  • Wrong line numbers on sources saved with CR+LF endingsis_newline() read PEEK_CURRENT to get the character following the one under examination, which only holds where the caller had already consumed it (the comment scanner and gravity_lexer_skip_line). In gravity_lexer_next the character was still the one at the current offset, so the CR of a CR+LF pair never saw the LF next to it: the pair counted as two line breaks and every row the compiler reported drifted by one per line read so far. An error on row 5 of a file written on Windows was reported on row 9. The lookahead is passed in explicitly now, and the string scanner keeps every byte of the terminator inside the token so that a literal spanning CR+LF lines is not shortened (#389, from the patch in #401)

Runtime

  • Heap buffer overflow in list_storeat — storing past the end of a list grows the backing array through marray_resize, which leaves the array untouched when the reallocation fails. The guard tested the pointer for NULL, but a failed realloc keeps the old, smaller, non-NULL buffer in place, so it never fired: the count was then set to the requested index and the fill loop wrote past the end of the allocation. The capacity actually obtained is checked instead, and the fill loop is bounded by it. Reachable from a script — x[4444444444444444444] = 0 is enough

Windows

  • The Windows code paths were guarded by WIN32, not _WIN32gravity_utils.h already selected windows.h and its DIRREF on _WIN32, while gravity_utils.c, gravity_opt_file.c and the CLI tested WIN32, which no compiler defines on its own. The Visual Studio projects define it in exactly two of their twelve configurations, so every x64 build compiled the POSIX bodies — opendir/readdir — against a DIRREF that is a HANDLE. MinGW and tcc land in the same place (#411)

Memory lifetime

  • Optional classes never releasedMath, File, JSON and ENV were leaked by every embedder that created and destroyed a VM, because gravity_core_free dropped a reference without the matching balance (#442)
  • Core reference leaked by every gravity_compiler_run — the compiler took a reference to the core classes on each run and never gave it back, so the count never reached zero and the core was never torn down (#442)
  • Double free of the inline source buffergravity -i handed its heap-allocated wrapper source to the compiler with is_static false, which passes ownership to the lexer; the lexer freed it and the CLI freed the same pointer again on the way out, aborting every inline run under a hardened allocator

Added

  • make staticlib builds libgravity.a from the same objects as make lib, so the archive carries the library without the CLI entry point (#427)
  • test/loadbuffer/ — malformed JSON executables that must each be rejected as a load error without crashing, plus json_bounds.c (make jsontest), 60 checks driving the JSON scanner directly. Run with test/loadbuffer/run_all.sh
  • test/unittest/bugfix_crlf_lineno.gravity — a source stored with CR+LF endings on purpose, asserting the row and column the compiler reports
  • A GitHub Actions workflow building with gcc and clang on Linux and macOS, plus a job built with -fsanitize=address,undefined that runs the unit tests, the fuzzing corpus and the loader tests through it

Changed

  • The usage text now prints the real default output name, gravity.g; README.md and CLAUDE.md documented a stale gravity.json
  • report_error carries the printf format attribute on gcc and clang in all three of its forms, so format/argument mismatches now fail the build rather than needing an external analyser. This replaces the CodeQL workflow, which still ran on github/codeql-action@v1, deprecated since January 2023, and reported green without being a current analysis. Its one open finding — a size_t passed to %d in gravity_codegen.c — is fixed

Verification

353/353 unit tests and 12/12 loader tests pass, and the unit tests plus the 742-input fuzzing corpus run clean under -fsanitize=address,undefined.

Thanks

To the reporters of #442, #443, #444, #446, #447 and #448 for the detailed write-ups and minimized reproducers, to @bardo84 for #389, and to @mwasplund (#401), @jockm (#427) and @tDwtp (#411) for the patches this release builds on.

Full Changelog: 0.9.7...0.9.8