Skip to content

Handle a decimal point longer than one byte - #773

Open
pargue wants to merge 1 commit into
akheron:masterfrom
pargue:fix/multibyte-decimal-point
Open

pargue wants to merge 1 commit into
akheron:masterfrom
pargue:fix/multibyte-decimal-point

Conversation

@pargue

@pargue pargue commented Nov 2, 2026

Copy link
Copy Markdown

Handle a decimal point longer than one byte

get_decimal_point() writes "1" and the locale's decimal point with sprintf into char buf[3] and returns buf[1]. In a locale whose decimal point is more than one byte, such as ps_AF.UTF-8 (U+066B, 2 bytes in UTF-8):

  • the sprintf writes 4 bytes into the 3-byte buffer (ASan: stack-buffer-overflow in get_decimal_point);
  • only the first byte of the point is used, so to_locale() puts a partial UTF-8 sequence where the . was, strtod() stops early, and json_loads("[1.5]") fails the assert in jsonp_strtod();
  • with USE_DTOA off, from_locale() makes the same one-byte assumption when printing.

It only happens when the program has set LC_NUMERIC to such a locale.

The change: get_decimal_point() returns the whole point as a string, in a buffer of MB_LEN_MAX bytes (the most one character can take). to_locale() grows the buffer when the point is longer than ., and jsonp_strtod() returns -2 if it cannot, which the lexer reports as "out of memory" instead of "real number overflow". from_locale() replaces the whole point and shortens the output.

Test: test_multibyte_decimal_point in test/suites/api/test_number.c loads reals under ps_AF.UTF-8 (including ones long enough to grow the buffer), dumps them, checks the output holds no byte of the locale's point, and reloads it in the C locale to compare values. It restores the caller's LC_NUMERIC. When the locale is not installed it prints that it skipped, which is likely on CI; locally it was built with localedef -i ps_AF -f UTF-8 <dir>/ps_AF.UTF-8 and run with LOCPATH=<dir>. On master it fails under ASan with the stack-buffer-overflow above; with the fix the full suite passes (215 tests) with USE_DTOA on and off, under ASan/UBSan.

Not changed here: as before, jsonp_strtod converts saved_text in place, so a "real number overflow near '...'" message under such a locale shows the locale's point rather than the input's .. Parsing with strtod_l and a C locale where available would avoid the rewrite entirely; that is a larger change than this fix.

get_decimal_point() wrote "1" and the locale's decimal point with
sprintf into char buf[3] and returned buf[1]. In a locale whose point is
more than one byte, such as ps_AF.UTF-8 (U+066B, 2 bytes in UTF-8), that
overflows buf by one byte, and only the first byte of the point is used:
to_locale() puts a partial UTF-8 sequence in place of '.', strtod()
stops early and json_loads() of a real such as 1.5 fails the assert in
jsonp_strtod(). With USE_DTOA off, from_locale() has the same
one-byte assumption when printing.

get_decimal_point() now returns the whole point as a string. to_locale()
grows the buffer when the point is longer than '.' and jsonp_strtod()
returns -2 if it cannot, which the lexer reports as out of memory.
from_locale() replaces the whole point and shortens the output.

The point buffer holds MB_LEN_MAX bytes, the most one character can take.

The test loads reals under ps_AF.UTF-8, dumps them, checks the output has
no byte of the locale's point, and reloads it in the C locale to compare
values. It restores the caller's LC_NUMERIC, and says it skipped when that
locale is not installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pargue
pargue force-pushed the fix/multibyte-decimal-point branch from 45e5ec4 to 493e968 Compare November 2, 2026 19:11
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.

1 participant