Repository navigation
U64toa 单元测试用例不通过 #123
Description
Activity
- added a commit that references this issue
on Feb 4, 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:12345 -> 12345 len=5 OK 1234567890123 -> 1234567890123 len=13 OK 18446744073709551615 -> 18446744073709551615 len=20 OK mismatches: 0Also 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: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 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;- the reporter says removing
-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:1 digit -> needs 1 byte of slack 2..8 -> 0 9..16 -> needs 8 bytes of slack 17..20 -> 0The 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.
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.
GCC(devtoolset 8.3.1) -O2优化编译(同时去掉-fsanitize=address)的时候,U64toa有部分测试用例结果不正确
[----------] 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)