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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ All notable changes to EigenScript are documented here.
Now routed through `eigs_json_parse_root`; regression test in
`embed_smoke` (the only consumer shape that can hit it).

- **json/store encode: magnitude checked before the double→int narrowing
cast (#816).** `store_json_encode`, the builtin `json_encode`,
`json_build`, and the json-path number formatter ran
`(int)n` before the range guard — UB for any number beyond int's range
(`store_put of [db, {"n": 1e300}]` reached it); correct output was a
hardware accident (x86-64 `cvttsd2si`). Same class as #695. The guard
now checks int's own range first, so the cast is always defined and
the encoded bytes are unchanged. `make asan` now also compiles with
`-fsanitize=float-cast-overflow` — GCC's `undefined` set does not
include it, which is why the existing sanitizer gate was silent on
this class.

## [0.38.0] - 2026-08-04

### Added
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ endef
VERDEF := -DEIGENSCRIPT_VERSION='"$(VERSION)"'
DEFS_OFF := -DEIGENSCRIPT_EXT_HTTP=0 -DEIGENSCRIPT_EXT_MODEL=0 -DEIGENSCRIPT_EXT_DB=0
MODEL_SRC := $(SRC_DIR)/model_io.c $(SRC_DIR)/model_infer.c $(SRC_DIR)/model_train.c
ASAN_FLAGS := -fsanitize=address,undefined -Werror=switch -g -O1
ASAN_FLAGS := -fsanitize=address,undefined,float-cast-overflow -Werror=switch -g -O1

SRC_V_release := $(SOURCES)
FLAGS_release := $(CFLAGS) $(DEFS_OFF) $(VERDEF)
Expand Down
16 changes: 13 additions & 3 deletions src/builtins.c
Original file line number Diff line number Diff line change
Expand Up @@ -820,7 +820,13 @@ static int eigs_json_encode_value(Value *v, strbuf *out, int depth) {
switch (v->type) {
case VAL_NUM: {
double n = v->data.num;
if (n == (int)n && fabs(n) < 1e15)
/* #816: magnitude BEFORE the narrowing cast (same class as
* #695) — converting a double beyond int's range is UB, and
* the old `n == (int)n && fabs(n) < 1e15` order ran the cast
* first. The bound is int's own range: integral values beyond
* it never took the %d path anyway (the equality failed), so
* output is unchanged and the cast is now always defined. */
if (fabs(n) < 2147483648.0 && n == (int)n)
strbuf_append_fmt(out, "%d", (int)n);
else
strbuf_append_fmt(out, "%.15g", n);
Expand Down Expand Up @@ -1342,7 +1348,10 @@ Value* builtin_json_build(Value *arg) {
Value *val = arg->data.list.items[i + 1];
if (val->type == VAL_NUM) {
double d = val->data.num;
if (d == (double)(int)d && d >= -1e9 && d <= 1e9)
/* #816: range BEFORE the cast — same class as the other two
* encoder sites; this variant shape was caught by the new
* float-cast-overflow gate in CI, not by pattern-grep. */
if (d >= -1e9 && d <= 1e9 && d == (double)(int)d)
strbuf_append_fmt(&out, "%d", (int)d);
else
strbuf_append_fmt(&out, "%.15g", d);
Expand Down Expand Up @@ -2220,7 +2229,8 @@ Value* builtin_json_path(Value *arg) {
if (current->type == VAL_NUM) {
char buf[64];
double d = current->data.num;
if (d == (double)(int)d && fabs(d) < 1e9)
/* #816: range before the cast (see json_build above). */
if (fabs(d) < 1e9 && d == (double)(int)d)
snprintf(buf, sizeof(buf), "%d", (int)d);
else
snprintf(buf, sizeof(buf), "%.15g", d);
Expand Down
8 changes: 7 additions & 1 deletion src/ext_store.c
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,13 @@ static void store_json_encode(Value *v, strbuf *out) {
switch (v->type) {
case VAL_NUM: {
double n = v->data.num;
if (n == (int)n && fabs(n) < 1e15)
/* #816: magnitude BEFORE the narrowing cast (same class as
* #695) — converting a double beyond int's range is UB, and
* the old `n == (int)n && fabs(n) < 1e15` order ran the cast
* first. The bound is int's own range: integral values beyond
* it never took the %d path anyway (the equality failed), so
* output is unchanged and the cast is now always defined. */
if (fabs(n) < 2147483648.0 && n == (int)n)
strbuf_append_fmt(out, "%d", (int)n);
else
strbuf_append_fmt(out, "%.15g", n);
Expand Down
10 changes: 10 additions & 0 deletions tests/test_json_roundtrip.eigs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,14 @@ ne is json_encode of nested
assert of [contains of [ne, "\"inner\""], "nested dict encode"]
assert of [contains of [ne, "99"], "nested dict value encode"]


# #816: encode at and beyond int's range — the guard must range-check
# before the narrowing cast. Output pinned at the boundary.
assert of [(json_encode of 2147483647) == "2147483647", "INT_MAX encodes via int path (#816)"]
assert of [(json_encode of 2147483648) == "2147483648", "2^31 encodes correctly past the int path (#816)"]
assert of [(json_encode of (0 - 2147483648)) == "-2147483648", "int min boundary encodes (#816)"]
assert of [(json_encode of 1e300) == "1e+300", "1e300 encodes, cast never runs (#816)"]
rtb is json_decode of json_encode of {"n": 1e300}
assert of [rtb.n == 1e300, "1e300 round-trips through encode/decode (#816)"]

print of "json roundtrip: all passed"
15 changes: 15 additions & 0 deletions tests/test_store.eigs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,21 @@ rm of "/tmp/eigs_store_short.db"
rm of "/tmp/eigs_store_badmagic.db"
rm of "/tmp/eigs_store_badver.db"

# #816: number encoding at and beyond int's range. The old guard ran the
# double->int cast BEFORE the magnitude check (UB past int's range; the
# fixed order checks first). These pin the output at the boundary — int
# path below 2^31, %.15g at and above it, huge magnitudes intact.
bdb is store_open of "/tmp/eigs816_bounds.db"
bk is store_put of [bdb, "nums", {"imax": 2147483647, "past": 2147483648, "imin": 0 - 2147483648, "huge": 1e300, "nhuge": 0 - 1e300}]
bn is store_get of [bdb, "nums", bk]
assert_eq of [bn.imax, 2147483647, "INT_MAX round-trips (#816)"]
assert_eq of [bn.past, 2147483648, "2^31 (past int) round-trips (#816)"]
assert_eq of [bn.imin, 0 - 2147483648, "int min boundary round-trips (#816)"]
assert_eq of [bn.huge, 1e300, "1e300 round-trips, cast never runs (#816)"]
assert_eq of [bn.nhuge, 0 - 1e300, "-1e300 round-trips (#816)"]
store_close of bdb
rm of "/tmp/eigs816_bounds.db"

# Clean up
rm of "/tmp/test_eigenstore.db"

Expand Down
Loading