Siyao Meng created HDDS-17260:
---------------------------------

             Summary: Snapshot diff can omit changed keys whose names differ 
from a nearby key by a supplementary character against a character in U+E000 to 
U+FFFF
                 Key: HDDS-17260
                 URL: https://issues.apache.org/jira/browse/HDDS-17260
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: CR-1-compare-snapshot-diff-keys-in-utf8-byte-order.patch

h3. Mechanism
OM tables store {{String}} keys as UTF-8, and RocksDB iterators, including the 
SST file readers that snapshot diff uses for the delta files, return them in 
unsigned byte order. The snapshot diff key merge compares the same keys with 
{{String.compareTo}}, which orders by UTF-16 code units, in two places:
* {{SstFileSetReader.MultipleSstFileIterator}} merges the keys of several delta 
SST files with a {{MinHeapMergeIterator}} built with {{Comparable::compareTo}}.
* {{TableMergeIterator.updateAndGetValueAtIndex}} looks each merged key up in 
the from and to snapshot tables. It seeks a table only when the key is greater, 
by {{String.compareTo}}, than the key that table's iterator holds from its last 
seek; otherwise it uses the held key and returns null when that key does not 
match.

The two orders agree except for names that differ at some position by a 
supplementary character (a surrogate pair, for example U+1F600, UTF-8 {{F0 9F 
98 80}}) against a character in U+E000 to U+FFFF (for example U+FF21, UTF-8 
{{EF BC A1}}). RocksDB puts the supplementary character after the other one, 
{{String.compareTo}} before it.

Around such names the merge goes wrong:
* In a single delta file the keys arrive in byte order. When the held key sorts 
after the next key in byte order but before it as a {{String}}, 
{{TableMergeIterator}} skips the seek and returns null for a key the table 
contains. If both lookups are then null, 
{{SnapshotDiffManager.addToObjectIdMap}} treats the key as unchanged and skips 
it.
* With several delta files the heap merge returns keys out of byte order and 
can return the same key more than once. {{TableMergeIterator}} then also gets 
keys that lie behind its last seek, and the skipped key can be one with only 
BMP characters, for example the U+E000 to U+FFFF name.

h3. Affected key names
Names in the same bucket and table that differ by a supplementary character 
against a character in U+E000 to U+FFFF, for example {{kA}} and {{k😀}}, or 
{{report(1).txt}} (fullwidth parentheses) and {{report😀}}, where at least one 
of them changed within the diff range. Supplementary characters include emoji 
and the CJK extension ideographs; U+E000 to U+FFFF covers the private use area, 
the CJK compatibility ideographs, the halfwidth and fullwidth forms and U+FFFD. 
Key name validation accepts these characters. Names that differ only in other 
characters are not affected.

h3. Impact
* The snapshot diff report can miss changed keys, for creates and deletes, 
without an error. The job finishes as DONE and the incomplete report is stored 
and served. Seen on FSO and OBS buckets, with the compaction DAG diff and with 
a forced full diff, all without the native RocksDB tools library. The native 
library path uses another per file reader but the same merge and the same 
{{TableMergeIterator}}; argued from code, not run.
* {{SnapshotDefragService.spillTableDiffIntoSstFile}} (incremental snapshot 
defragmentation, off by default) reads the delta files through the same 
{{SstFileSetReader}} and {{TableMergeIterator}}. By code, a defragmented 
snapshot can then miss changed keys; argued, not run.
* Affects 2.2.0 and 2.2.1. The {{TableMergeIterator}} seek check came with 
HDDS-13930; up to 2.1.x the diff looked every key up with {{Table.get}}, which 
does not depend on the key order.
* Proposed priority: Major. The output is silently incomplete, but no live data 
is lost and only names with the character combination above are affected.

h3. Reproduction
No standalone reproduction is attached. The regression tests in the patch cover 
the defect; each fails without the change:
* {{TestSstFileSetReader.testKeysMergedInUtf8ByteOrder}} merges two real SST 
files whose keys straddle the boundary (a common prefix, ASCII, U+00E9, U+D7FF, 
U+E000, U+FF21, U+10000, U+1F600). Without the change it returns keys out of 
order and twice: {{[k, kz, ké, k퟿, k, k𐀀, k, kA, k𐀀, k😀]}} (the U+E000 
character is invisible) instead of each key once in byte order.
* {{TestTableMergeIterator.testWithKeysInUtf8ByteOrder}} looks up {{k}}, 
{{kA}}, {{k😀}} in two RocksDB tables. With {{String.compareTo}} as the order it 
returns null for {{k😀}} from the table that contains it ({{[null, value3]}} 
instead of {{[value2, value3]}}).
* {{OmSnapshotTests.testSnapDiffWithKeysInUtf8ByteOrder}} creates and then 
deletes {{kA}} and {{k😀}} between snapshots on a mini cluster. Without the 
change the reports are {{[+ ./kA, + ./kz]}} and {{[- ./kA]}}, without {{k😀}}.

h3. Patch
[^CR-1-compare-snapshot-diff-keys-in-utf8-byte-order.patch], against 
b10a7d427bdf83807a7e48a1be507d3b02d5a503. It also applies to master at 
d3b5f518556.

The patch adds {{StringUtils.compareUtf8}}, which orders well formed strings 
like their UTF-8 bytes (Unicode code point order) without encoding them: it 
differs from {{String.compareTo}} only when exactly one of the first differing 
code units is a surrogate, which it then sorts last. {{SstFileSetReader}} 
passes it to {{MinHeapMergeIterator}} instead of {{Comparable::compareTo}}. 
{{TableMergeIterator}} gets a constructor that takes the key order for its seek 
check, and both callers, {{SnapshotDiffManager.addToObjectIdMap}} and 
{{SnapshotDefragService.spillTableDiffIntoSstFile}}, pass 
{{StringUtils::compareUtf8}}. The existing {{TableMergeIterator}} constructor 
keeps the natural order. For names without the character combination above the 
order, and so the behaviour, is unchanged; there is no config, wire or on disk 
change.

With the change the three tests above pass, as do the existing 
{{TestSstFileSetReader}}, {{TestMinHeapMergeIterator}}, 
{{TestTableMergeIterator}}, {{TestSnapshotDiffManager}}, 
{{TestSnapshotDefragService}} and {{TestRocksDiffUtils}} suites and the 
{{OmSnapshotTests}} subclasses that run without the native library. The native 
library subclasses were not run.

Found by code review of the OM snapshot diff paths, as part of the TLA+ 
verification effort under HDDS-15926, on commit 
b10a7d427bdf83807a7e48a1be507d3b02d5a503. Checked against HDDS issues and 
apache/ozone pull requests for duplicates before filing. The attached patch is 
a proposal for review. Generated with Specula (Claude Opus 5.5).



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to