You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[----------] 1 test from U64toa
[ RUN ] U64toa.Basic
sonic-cpp.git/tests/itoa_test.cpp:34: Failure
Expected equality of these values:
expect.data()
Which is: "12345"
buf
Which is: "10345"
sonic-cpp.git/tests/itoa_test.cpp:34: Failure
Expected equality of these values:
expect.data()
Which is: "1234567890123"
buf
Which is: "1034567890123"
[ FAILED ] U64toa.Basic (0 ms)
[----------] 1 test from U64toa (0 ms total)
I tried to reproduce this and could not on Apple clang 21 / arm64, but the investigation turned up something that may explain the GCC 8.3.1 result — and a separate detail about U64toa's buffer contract that seems worth recording either way.
Not reproducible with clang -O2
Compiling the real U64toa from include/sonic/internal/itoa.h at -O2, both reported values are correct, along with every leading-1 length from 1 to 20 digits:
12345 -> 12345 len=5 OK
1234567890123 -> 1234567890123 len=13 OK
18446744073709551615 -> 18446744073709551615 len=20 OK
mismatches: 0
Also clean under -fsanitize=address,undefined. So this is not architecture-independent, and I cannot confirm or deny the GCC 8.3.1 behaviour — I have no GCC on this machine (gcc here is an Apple clang shim). Treating that as a limitation, not as evidence against the report.
Why 10345 is a plausible miscompile symptom, not a random corruption
The reported 12345 → 10345 corrupts exactly the second byte, and 1234567890123 → 1034567890123 corrupts exactly the same position. That is not arbitrary — it is the byte the leading-zero-skip trick deliberately writes and expects to be overwritten.
For 12345, Utoa_1_8 takes the 5–6 digit branch with hi = 1, so lz = 1 and it copies two bytes from kDigits[hi*2 + lz] = index 3:
kDigits[0..7] = "00010203"
index 3,4 = '1','0'
It then advances only 2 - lz = 1 byte, so the stray '0' must be overwritten by the following Copy2Digs. The reported output is precisely what you get if that subsequent write does not land — the '1' survives and the '0' is never replaced.
That makes the symptom consistent with the compiler mis-ordering or eliding one of the overlapping 2-byte stores, rather than with a logic error in the branch. Two things support that reading:
itoa.h:48-49 already carries #pragma GCC diagnostic ignored "-Warray-bounds" around exactly this function, so GCC is known to be unhappy about these accesses;
the reporter says removing -fsanitize=address is what exposes it, which is the classic signature of an optimisation-dependent issue rather than a plain bug.
If that is right, -fno-strict-aliasing or making Copy2Digs write through a memcpy of 2 bytes would be the things to try on that toolchain. I can't test either claim here, so I'd rather flag the hypothesis than assert it.
Separate finding: U64toa writes past its returned end pointer
While testing I sized buffers exactly to the digit count and let ASan watch. U64toa overruns for some lengths:
The 9–16 range is the Utoa_8 path; ASan reports the offender as a WRITE of size 16, i.e. a vectorised store covering more than the digits produced.
This is not a live bug — I checked the real callers before concluding anything. serialize.h:94 does wb.Grow(kNumberSize) with kNumberSize = 33 (serialize.h:45), which comfortably covers 20 digits plus the widest overrun measured. And tests/itoa_test.cpp already comments "reserved buffer should be 32 bytes". So the over-write is an intended part of the contract.
The reason I mention it: that contract is currently expressed only in a test comment and an implicit 33. Anyone calling U64toa directly from new code — it is in internal, but the header is shipped — would reasonably size a buffer to the return value and get a heap overflow for 9–16 digit values. A one-line comment on U64toa stating the minimum reservation would make that safe to use.
Happy to send a PR for just that comment if you think it is worth having. I have deliberately not touched the algorithm, since the reported failure is toolchain-specific and I cannot verify a fix for it here.
Probes used are self-contained if anyone wants to re-run them on GCC 8.3.1 — that is the one data point that would settle the original report.
Following up on the U64toa buffer question I raised earlier — I went and measured it, and there is no bug there. Recording the numbers so nobody else has to re-derive them:
I instrumented the write extent with a canary buffer under ASan, across double max/min, denormals, 1e±300, u64 max, i64 min and the branch boundaries, in both default and kSerializeFloatFormatJava modes. Worst case touched 24 bytes, against the 33 that serialize.h reserves via wb.Grow(kNumberSize). So the slack the digit-copy relies on is comfortably covered, and my earlier concern about the overwrite-dependent Copy2Digs was unfounded for every real call site.
I also round-tripped F64toa through strtod over ~840k values (random bit patterns, powers of ten and their nextafter neighbours, hand-picked hard cases) in both float formats, comparing bit patterns: zero mismatches.
The original GCC 8.3.1 -O2 report stands as I described it before — I still cannot reproduce it, having no real GCC on this machine, and that remains an environment limitation rather than a refutation.
While auditing that path I did find an unrelated, reproducible defect in the JSONPath on-demand backend and opened #152 for it: a negative array index returns kErrorNoneNoMatch, the same code as an absent key, so callers cannot distinguish "unsupported path" from "member not present". That one is independent of this issue.
Activity
[-]U64toa 单元测试用例结果不正确[/-][+]U64toa 单元测试用例不通过[/+]fix: resolve multiple issues - itoa bug, sorted keys, SetRaw API, mem…
linhongyu510 commentedon Sep 17, 2026
I tried to reproduce this and could not on Apple clang 21 / arm64, but the investigation turned up something that may explain the GCC 8.3.1 result — and a separate detail about
U64toa's buffer contract that seems worth recording either way.Not reproducible with clang -O2
Compiling the real
U64toafrominclude/sonic/internal/itoa.hat-O2, both reported values are correct, along with every leading-1length from 1 to 20 digits:Also clean under
-fsanitize=address,undefined. So this is not architecture-independent, and I cannot confirm or deny the GCC 8.3.1 behaviour — I have no GCC on this machine (gcchere is an Apple clang shim). Treating that as a limitation, not as evidence against the report.Why
10345is a plausible miscompile symptom, not a random corruptionThe reported
12345→10345corrupts exactly the second byte, and1234567890123→1034567890123corrupts exactly the same position. That is not arbitrary — it is the byte the leading-zero-skip trick deliberately writes and expects to be overwritten.For
12345,Utoa_1_8takes the 5–6 digit branch withhi = 1, solz = 1and it copies two bytes fromkDigits[hi*2 + lz]= index 3:It then advances only
2 - lz= 1 byte, so the stray'0'must be overwritten by the followingCopy2Digs. The reported output is precisely what you get if that subsequent write does not land — the'1'survives and the'0'is never replaced.That makes the symptom consistent with the compiler mis-ordering or eliding one of the overlapping 2-byte stores, rather than with a logic error in the branch. Two things support that reading:
itoa.h:48-49already carries#pragma GCC diagnostic ignored "-Warray-bounds"around exactly this function, so GCC is known to be unhappy about these accesses;-fsanitize=addressis what exposes it, which is the classic signature of an optimisation-dependent issue rather than a plain bug.If that is right,
-fno-strict-aliasingor makingCopy2Digswrite through amemcpyof 2 bytes would be the things to try on that toolchain. I can't test either claim here, so I'd rather flag the hypothesis than assert it.Separate finding:
U64toawrites past its returned end pointerWhile testing I sized buffers exactly to the digit count and let ASan watch.
U64toaoverruns for some lengths:The 9–16 range is the
Utoa_8path; ASan reports the offender as aWRITE of size 16, i.e. a vectorised store covering more than the digits produced.This is not a live bug — I checked the real callers before concluding anything.
serialize.h:94doeswb.Grow(kNumberSize)withkNumberSize = 33(serialize.h:45), which comfortably covers 20 digits plus the widest overrun measured. Andtests/itoa_test.cppalready comments "reserved buffer should be 32 bytes". So the over-write is an intended part of the contract.The reason I mention it: that contract is currently expressed only in a test comment and an implicit
33. Anyone callingU64toadirectly from new code — it is ininternal, but the header is shipped — would reasonably size a buffer to the return value and get a heap overflow for 9–16 digit values. A one-line comment onU64toastating the minimum reservation would make that safe to use.Happy to send a PR for just that comment if you think it is worth having. I have deliberately not touched the algorithm, since the reported failure is toolchain-specific and I cannot verify a fix for it here.
Probes used are self-contained if anyone wants to re-run them on GCC 8.3.1 — that is the one data point that would settle the original report.
linhongyu510 commentedon Sep 18, 2026
Following up on the
U64toabuffer question I raised earlier — I went and measured it, and there is no bug there. Recording the numbers so nobody else has to re-derive them:I instrumented the write extent with a canary buffer under ASan, across double max/min, denormals,
1e±300,u64max,i64min and the branch boundaries, in both default andkSerializeFloatFormatJavamodes. Worst case touched 24 bytes, against the 33 thatserialize.hreserves viawb.Grow(kNumberSize). So the slack the digit-copy relies on is comfortably covered, and my earlier concern about the overwrite-dependentCopy2Digswas unfounded for every real call site.I also round-tripped
F64toathroughstrtodover ~840k values (random bit patterns, powers of ten and theirnextafterneighbours, hand-picked hard cases) in both float formats, comparing bit patterns: zero mismatches.The original GCC 8.3.1
-O2report stands as I described it before — I still cannot reproduce it, having no real GCC on this machine, and that remains an environment limitation rather than a refutation.While auditing that path I did find an unrelated, reproducible defect in the JSONPath on-demand backend and opened #152 for it: a negative array index returns
kErrorNoneNoMatch, the same code as an absent key, so callers cannot distinguish "unsupported path" from "member not present". That one is independent of this issue.