Skip to content

Commit f895f6f

Browse files
committed
fix(text-json): a misaligned u16 literal UBSan found, and #1894 closes on a rule-14 sweep
#1894's gate went stale the same day. Its own note set the condition -- "close the negative-fixture half as not applicable if #1899 is declined/wontfix; do not invent Text.Json sites while #1888 remains declined" -- and #1899 was declined on 2026-08-19 while #1888 remains declined, so both became determinate. NEGATIVE-FIXTURE HALF: NOT APPLICABLE, verified rather than trusted. A probe against the shipped headers confirms all four spellings #1888/#1899 would have outlawed are STILL LEGAL: JsonArray copy- and move-constructible, JsonNode::DetachParent() callable, and Extensions::Ancestors returning std::vector<XElement*>. A negative fixture asserts a spelling is REJECTED BY THE COMPILER; with nothing rejected, writing one would mean inventing an outlawed spelling to pin. Not a claim that CCF-019 produced no fixtures at all -- its async members did, in #1959 -- only that the two owned-tree modules #1894 scoped outlawed nothing. SANITIZER HALF: RE-MEASURED, AND THE RE-MEASUREMENT FOUND A REAL DEFECT. The recorded figures were 218/218 Text_Json and 184/184 Xml_Linq from 2026-07-31; both suites have since grown to 302 and 349, so they described a tree that no longer exists. ASan+LSan came back clean on both. UBSan ABORTED Text_Json on a misaligned reference binding in char_traits<char16_t>::length. It is a defect in a TEST, not in production code, and finding which construction caused it needed measurement rather than reading: the array copied verbatim into a standalone probe did NOT reproduce, and neither did any literal alone. The manifestation is translation-unit-layout dependent -- the empty u"" literal is merged into a 1-byte-aligned mergeable section beside narrow literals, so length() binds a const char16_t& to an odd address. One line: u"" becomes std::u16string(), the same empty string with no literal to misalign. The mutation that restores the literal reproduces the report. The instrumentation was shown able to report before any of this was believed (the #1957/SR-AUD-204 lesson): deliberate defects built with the same flags gave one report each for ASan, LSan and UBSan, and the no-defect control gave none. docs/NegativeConsumerFixtureValidation.md section 22 records all of it, and corrects that document's running total -- stale since 2026-08-04 at 11 fixtures/94 sites -- to the measured 45/231. Gate: 17,585 run, 17,585 passed, 0 failed, 0 skipped across 38 executables (+0, deliberately -- a test was repaired, not added). Build directories: build/ (gate), build-asan (reused), build-ubsan (created), all --parallel 2 with ccache.
1 parent 848a951 commit f895f6f

4 files changed

Lines changed: 116 additions & 2 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/NegativeConsumerFixtureValidation.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,3 +1447,117 @@ the pool at all already required a default-constructible `T`, and pinning the ca
14471447
**11 fixtures / 94 sites.** The bounded checker run over this fixture alone:
14481448
`OK: 1 negative consumer fixture(s), 13 negative site(s), every site rejected (g++ 13.3.0,
14491449
14 compiler invocation(s), peak 2 job(s), 5.4s)`.
1450+
1451+
## 22. Ticket #1894 — the CCF-019 fixtures that have nothing to reject (2026-08-19)
1452+
1453+
**#1894 closes with its negative-fixture half NOT APPLICABLE and its sanitizer half re-measured.**
1454+
It is the only ticket in this document whose fixtures were never written, and the reason is worth
1455+
recording rather than leaving as an absence.
1456+
1457+
### 22.1 Why there is nothing to pin
1458+
1459+
#1894 exists to add *"one negative consumer fixture site per spelling the CCF-019 implementation
1460+
tickets outlaw"*. Its own 2026-07-31 analysis concluded that **no CCF-019 repair has outlawed any
1461+
spelling** in `text-json` or `xml-linq` — every landed repair there (#1886, #1887, #1890, #1891,
1462+
#1895, #1898) is source-compatible by construction. The two that *would* have created outlawed
1463+
spellings are **#1888** (delete `JsonNode`'s copy/move and make `DetachParent` non-public) and
1464+
**#1899** (return non-owning view types from `Ancestors`/`AncestorsAndSelf`).
1465+
1466+
**Both are now declined.** #1888 was declined earlier; **#1899 was declined on 2026-08-19**, which
1467+
is the condition #1894's own note set for this classification: *"close the negative-fixture half as
1468+
not applicable if #1899 is declined/wontfix; do not invent Text.Json sites while #1888 remains
1469+
declined."*
1470+
1471+
**Verified rather than taken on trust.** A probe compiled against the shipped headers confirms all
1472+
four spellings are still legal today:
1473+
1474+
| spelling | ticket that would outlaw it | status |
1475+
|---|---|---|
1476+
| `JsonArray` copy-constructible | #1888 | **legal** |
1477+
| `JsonArray` move-constructible | #1888 | **legal** |
1478+
| `JsonNode::DetachParent()` callable | #1888 | **legal** |
1479+
| `Extensions::Ancestors` returns `std::vector<XElement*>` | #1899 | **legal** |
1480+
1481+
A negative fixture asserts that a spelling is *rejected by the compiler*. With nothing rejected,
1482+
writing one would mean **inventing an outlawed spelling to pin**, which is the opposite of what
1483+
these fixtures are for. Neither `test/consumer/text_json_node_lifetime_negative.cpp` nor
1484+
`test/consumer/xml_linq_object_lifetime_negative.cpp` exists, and neither should.
1485+
1486+
**This is not a claim that CCF-019 produced no fixtures at all.** Its *async* members did: #1959
1487+
landed a public source break in three spellings with
1488+
`test/consumer/threading_borrowed_callback_negative.cpp`. What #1894 scoped — the two owned-tree
1489+
modules — is where nothing was outlawed.
1490+
1491+
### 22.2 The sanitizer half, re-measured because the recorded figures were stale
1492+
1493+
#1894's note recorded a clean ASan+UBSan+LSan run over **218/218** `SharpRuntimeTests_Text_Json`
1494+
and **184/184** `SharpRuntimeTests_Xml_Linq`, from 2026-07-31. Both suites have grown a great deal
1495+
since — through #1897, #2115, #2117, #2118, #2119, #2199, #2200, #2201, #1896, #2350 and others —
1496+
so the recorded figures are evidence about a tree that no longer exists. Re-run on 2026-08-19:
1497+
1498+
| suite | ASan + LSan | UBSan |
1499+
|---|---|---|
1500+
| `SharpRuntimeTests_Text_Json` | **302 / 302, 0 reports** | **302 / 302, 0 reports** — after one repair, §22.2b |
1501+
| `SharpRuntimeTests_Xml_Linq` | **349 / 349, 0 reports** | **349 / 349, 0 reports** |
1502+
1503+
ASan was run with `detect_leaks=1`, `detect_stack_use_after_return=1`, `strict_string_checks=1`,
1504+
`check_initialization_order=1` and `detect_odr_violation=2`. UBSan was built with
1505+
`-fno-sanitize-recover=undefined`, so a violation aborts rather than printing and continuing.
1506+
1507+
### 22.2b The re-measurement found a real defect that the stale figures had hidden
1508+
1509+
**UBSan was not clean on `SharpRuntimeTests_Text_Json`, and that is the whole reason for
1510+
re-measuring rather than citing the 2026-07-31 note.** The suite **aborted** — the tree is built
1511+
with `-fno-sanitize-recover=undefined`, so a violation ends the run rather than printing and
1512+
continuing:
1513+
1514+
```
1515+
/usr/include/c++/14/bits/char_traits.h:793:20: runtime error: reference binding to misaligned
1516+
address 0x... for type 'const char_type', which requires 2 byte alignment
1517+
#0 std::char_traits<char16_t>::length(char16_t const*)
1518+
#1 std::u16string::basic_string(char16_t const*, allocator const&)
1519+
#2 JsonEncodedTextTests_BOTHOverloadsAgreeOnEveryInputClassTheyCanBOTHExpress_Test::TestBody()
1520+
```
1521+
1522+
**It is a defect in a test, not in production code**, and finding *which* construction caused it
1523+
took measurement rather than reading: the array was copied verbatim into a standalone probe and
1524+
**did not reproduce**, and so did every individual literal in it. The manifestation is
1525+
**translation-unit-layout dependent** — the reported address holds a run of NUL bytes inside the
1526+
narrow string pool, which is the empty `u""` literal merged into a **1-byte-aligned** mergeable
1527+
section alongside narrow literals. `char_traits<char16_t>::length` then binds a
1528+
`const char16_t&` to an odd address to read the terminator. The value it computes is correct; the
1529+
reference binding is not.
1530+
1531+
The repair is one line — `u""` becomes `std::u16string()`, which is the same empty UTF-16 string
1532+
with no literal to misalign, so the case's coverage is unchanged:
1533+
1534+
| | before | after |
1535+
|---|---|---|
1536+
| `SharpRuntimeTests_Text_Json` under UBSan | **aborted, 1 runtime error** | **302 / 302, 0 errors** |
1537+
| the same suite in the ordinary build | 302 / 302 | 302 / 302 |
1538+
1539+
**Mutation**: restoring the `u""` literal reproduces the report (1 error). Caught.
1540+
1541+
### 22.3 The instrumentation was shown able to report
1542+
1543+
A clean run is only evidence about the *code* if a dirty run is evidence about the *sanitizer*
1544+
the lesson #1957/SR-AUD-204 recorded when a silent TSan turned out to say nothing. So each
1545+
sanitizer was given a deliberate defect built with the same flags:
1546+
1547+
| probe | result |
1548+
|---|---|
1549+
| heap-buffer-overflow under ASan | **1 report — instrumentation live** |
1550+
| unfreed allocation under LSan | **1 report — instrumentation live** |
1551+
| shift exponent ≥ width under UBSan | **1 report — instrumentation live** |
1552+
| no defect (control) | **0 reports — correctly silent** |
1553+
1554+
### 22.4 A correction to this document's running total
1555+
1556+
§21.5 records **11 fixtures / 94 sites**, and that has been stale since 2026-08-04: the measured
1557+
total today is **45 fixtures / 231 sites**. The ~34 fixtures added between #2054 and now were each
1558+
recorded in their own ticket's migration note rather than here, so the record exists — it is this
1559+
document's *running total* that fell behind, not the evidence. #1894 adds no fixture of its own, so
1560+
the total is unchanged by this section.
1561+
1562+
Build directories used: `build-asan` (reused) and `build-ubsan` (created), both `--parallel 2`,
1563+
both with `ccache`.

modules/text-json/tests/System/Text/Json/JsonNamespaceReviewTests.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ TEST(JsonEncodedTextTests, BOTHOverloadsAgreeOnEveryInputClassTheyCanBOTHExpress
492492
struct Pair { const char* name; std::string narrow; std::u16string wide; };
493493
const Pair pairs[] = {
494494
{"ASCII", std::string("hello"), u"hello"},
495-
{"empty", std::string(""), u""},
495+
{"empty", std::string(""), std::u16string()},
496496
{"U+00E9", std::string("\xC3\xA9"), u"é"},
497497
{"U+1F600", std::string("\xF0\x9F\x98\x80"), u"\U0001F600"},
498498
{"lone HIGH surrogate", std::string("\xED\xA0\x80"), std::u16string(1, 0xD800)},

plan.sqlite3

0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)