[
https://issues.apache.org/jira/browse/CASSANDRA-21751?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Brandon Williams updated CASSANDRA-21751:
-----------------------------------------
Bug Category: Parent values: Correctness(12982)Level 1 values: Recoverable
Corruption / Loss(12986)
Complexity: Normal
Component/s: Legacy/Local Write-Read Paths
Discovered By: User Report
Fix Version/s: 5.0.x
6.x
7.x
Severity: Normal
Status: Open (was: Triage Needed)
> Decimal validation accepts a 4-byte value (a scale with no unscaled value),
> which Cassandra itself cannot read, compare, flush or compact
> -----------------------------------------------------------------------------------------------------------------------------------------
>
> Key: CASSANDRA-21751
> URL: https://issues.apache.org/jira/browse/CASSANDRA-21751
> Project: Apache Cassandra
> Issue Type: Bug
> Components: Legacy/Local Write-Read Paths
> Reporter: Nadav Har'El
> Priority: Normal
> Fix For: 5.0.x, 6.x, 7.x
>
>
> A {{decimal}} value is a 4-byte {{int}} scale followed by a {{varint}}
> unscaled value. {{DecimalSerializer.validate()}} only rejects non-empty
> values shorter than 4 bytes, so it accepts a 4-byte value consisting of the
> scale alone. Nothing in Cassandra can interpret such a value:
> {{DecimalSerializer.deserialize()}} passes the empty remainder to {{{}new
> BigInteger(byte[]){}}}, which throws {{{}NumberFormatException: Zero length
> BigInteger{}}}. As a result, one such value, inserted with
> {{blobAsDecimal()}} or as a bound value, causes server errors on reads and
> writes. As a clustering key it also makes memtable flush and compaction of
> the table fail.
> Tested on 5.0.9 and 6.0-alpha2, with identical results.
> h3. Reproducer
> {code:sql}
> CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy',
> 'replication_factor': 1};
> CREATE TABLE ks.t (p int, c decimal, v decimal, PRIMARY KEY (p, c));
> -- Accepted (also accepted as a 4-byte bound value):
> INSERT INTO ks.t (p, c, v) VALUES (1, 1, blobAsDecimal(0x00000000));
> SELECT decimalAsBlob(v) FROM ks.t WHERE p = 1; -- 0x00000000, stored as is
> -- Every operation that interprets the value fails with
> -- ServerError: java.lang.NumberFormatException: Zero length BigInteger
> SELECT CAST(v AS text) FROM ks.t WHERE p = 1;
> SELECT JSON p, c, v FROM ks.t WHERE p = 1;
> UPDATE ks.t SET v = 2 WHERE p = 1 AND c = 1 IF v = 0;
> -- As a clustering key:
> INSERT INTO ks.t (p, c) VALUES (2, blobAsDecimal(0x00000000)); -- accepted
> INSERT INTO ks.t (p, c) VALUES (2, 1); --
> WriteFailure
> SELECT decimalAsBlob(c) FROM ks.t WHERE p = 2 AND c = 0; -- ReadFailure
> {code}
> Then {{nodetool flush ks t}} fails with "error: Zero length BigInteger". The
> same failure happens with arithmetic ({{{}v + 1{}}}) and {{{}sum(v){}}}.
> h3. Impact
> # *Server errors on reads:* {{{}CAST{}}}, {{{}SELECT JSON{}}}, arithmetic,
> {{sum()}} and LWT conditions on the column all fail with a server error
> rather than a clean client error.
> # *The partition stops accepting writes:* once a 4-byte clustering key is in
> a partition, inserting any other clustering key into that partition fails
> (WriteFailure). The memtable insert has to compare the new key with the
> 4-byte one, and that comparison throws. Reads that compare against the key
> (e.g. {{WHERE c = 0}} or {{{}c > -1{}}}) fail with ReadFailure.
> # *Flush fails:* {{MetadataCollector.updateClusteringValues()}} compares
> every row's clustering key with the min/max so far. So a memtable holding a
> 4-byte clustering key plus any other row of the table, even in another
> partition, cannot be flushed:
> {noformat}
> ERROR [PerDiskMemtableFlushWriter_0:2] JVMStabilityInspector -- Exception in
> thread Thread[PerDiskMemtableFlushWriter_0:2,...]
> java.lang.NumberFormatException: Zero length BigInteger
> at java.base/java.math.BigInteger.<init>(Unknown Source)
> at
> org.apache.cassandra.serializers.DecimalSerializer.deserialize(DecimalSerializer.java:38)
> at
> org.apache.cassandra.db.marshal.AbstractType.compose(AbstractType.java:133)
> at
> org.apache.cassandra.db.marshal.AbstractType.compareComposed(AbstractType.java:115)
> at
> org.apache.cassandra.db.marshal.DecimalType.compareCustom(DecimalType.java:88)
> at
> org.apache.cassandra.db.marshal.AbstractType.compare(AbstractType.java:214)
> at
> org.apache.cassandra.db.ClusteringComparator.compareComponent(ClusteringComparator.java:191)
> at
> org.apache.cassandra.db.ClusteringComparator.compare(ClusteringComparator.java:148)
> at
> org.apache.cassandra.io.sstable.metadata.MetadataCollector.updateClusteringValues(MetadataCollector.java:311)
> at
> org.apache.cassandra.io.sstable.format.SortedTableWriter.addRow(SortedTableWriter.java:213)
> ...
> {noformat}
> After the failed flush, the next {{nodetool flush}} of the table reports
> success without writing anything, and the rows of the failed memtable are
> still served from memory. When the table was later dropped, Cassandra logged
> "Failed to force-recycle all segments; at least one segment is still in use
> with dirty CFs." I did not test what commit log replay does after a restart.
> # *Compaction fails:* if the 4-byte key was flushed alone into its own
> sstable, compacting that sstable with any other sstable of the table fails
> with the same exception, from {{CompactionTask.runMayThrow()}} through
> {{{}MetadataCollector.updateClusteringValues(){}}}.
> # *Clients cannot read it:* the Java driver 3.12.1 bundled with Cassandra
> throws {{NumberFormatException: Zero length BigInteger}} from
> {{{}TypeCodec.decimal().deserialize(){}}}. The Python driver fails to decode
> the result column ("invalid literal for int() with base 16") and then marks
> the whole connection defunct. ScyllaDB's fork of gocql (v1.18.3) explicitly
> rejects it ("the length of the data should be 0 or more than 5").
> h3. Why this is a validation gap, not a legitimate encoding of zero
> One might argue that a 4-byte decimal is a valid encoding of zero, and that
> {{deserialize()}} should be fixed to accept it instead. It should not:
> # *The protocol spec defines zero as one byte.* Section 5.6 of
> native_protocol_v5.spec defines a decimal as a scale "followed by a varint
> encoding (see section 5.24) of the unscaled value". Section 5.24's example
> table encodes 0 as {{{}0x00{}}}. No varint is encoded as zero bytes.
> # *Cassandra's own varint type treats empty bytes as "empty", not as 0.*
> {{IntegerSerializer.deserialize()}} returns {{null}} for an empty value, and
> {{IntegerType}} sorts the empty value before all numbers. On 5.0.9 and
> 6.0-alpha2, a {{varint}} column set to {{blobAsVarint(0x)}} reads back as
> null, {{CAST(i AS text)}} and {{i + 1}} return null, and {{SELECT JSON}}
> returns an empty string (see below). Reading a decimal's empty unscaled
> varint as 0 would contradict how the same varint encoding is read everywhere
> else in Cassandra.
> {code:java}
> SELECT JSON i FROM ks.v WHERE p = 1; -- {"i": ""}
> {code}
> # *No writer produces it.* {{DecimalSerializer.serialize()}} writes
> {{{}BigInteger.toByteArray(){}}}, which is never empty (zero is
> {{{}0x00{}}}), and drivers do the same. The only ways to create a 4-byte
> decimal are {{blobAsDecimal()}} or a hand-crafted bound value.
> # *The 4-byte lower bound was never a format decision.* Until 2013,
> {{validate()}} did nothing ("no useful check for invalid decimals"). The
> current check was added during the CASSANDRA-5744 cleanup with the comment
> "We at least store the scale", i.e. as a minimal sanity check. Nothing
> indicates that a value with no unscaled part was considered and allowed.
> # *Nothing can depend on it.* Every code path that interprets such a value
> already fails, both in Cassandra and in the drivers. So rejecting it at write
> time cannot break any working application. It only turns a later server
> error, or a flush or compaction failure, into an immediate
> {{{}InvalidRequest{}}}.
> h3. Suggested fix
> In {{{}DecimalSerializer.validate(){}}}, reject non-empty values shorter than
> 5 bytes (4-byte scale plus at least one byte of unscaled value), e.g.
> "Expected 0 or at least 5 bytes".
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]