sepuri sai krishna created CASSANDRA-21549:
----------------------------------------------

             Summary: Masked columns cannot be deserialized from cluster 
metadata, preventing node startup
                 Key: CASSANDRA-21549
                 URL: https://issues.apache.org/jira/browse/CASSANDRA-21549
             Project: Apache Cassandra
          Issue Type: Bug
          Components: Feature/Dynamic Data Masking, Transactional Cluster 
Metadata
            Reporter: sepuri sai krishna
            Assignee: sepuri sai krishna


h2. Summary

{{ColumnMask.Serializer.deserialize}} has two independent defects that make any 
table with a
dynamic data masked column unreadable from a serialized {{ClusterMetadata}}. A 
node that has taken
a cluster metadata snapshot then fails to start.

h2. Details

In {{src/java/org/apache/cassandra/cql3/functions/masking/ColumnMask.java}}:

{code:java}
List<AbstractType<?>> argTypes = new ArrayList<>(numArgs + 1);
argTypes.set(0, columnType);                                             // (1)
ByteBuffer[] partialArgValues = new ByteBuffer[numArgs];
for (int i = 0; i < numArgs; i++)
{
    AbstractType<?> argType = CQLTypeParser.parse(keyspace, in.readUTF(), 
types);
    argTypes.set(i + 1,  argType);                                       // (1)
    boolean valuePresent = in.readBoolean();
    partialArgValues[i] = valuePresent ? null : 
ByteBufferUtil.readWithVIntLength(in);  // (2)
}
{code}

# {{new ArrayList<>(numArgs + 1)}} sets the *capacity*, not the size, so the 
list is empty and {{set(0, ..)}}
throws {{IndexOutOfBoundsException}}. This fires for every masked column, 
including
{{MASKED WITH DEFAULT}}.
# The ternary is inverted: the argument is discarded when present, and a read 
is attempted when
absent. Besides losing the value this desynchronises the input stream, 
corrupting every field
deserialized afterwards. With defect (1) fixed in isolation, the same tests 
fail with
{{EOFException: EOF after 43 bytes out of 1024}}.

h2. Impact

{{ColumnMask.serializer}} is reached through
{{ColumnMetadata -> TableMetadata -> Tables -> KeyspaceMetadata -> 
DistributedSchema -> ClusterMetadata}}.
Ordinary DDL does not hit it, because schema changes propagate as 
transformations that each node
replays locally. It is reached when the schema is embedded in a serialized 
{{ClusterMetadata}},
i.e. for cluster metadata snapshots, which are read back on startup
({{LocalLog.replayPersisted}}) and when a lagging peer catches up from the CMS.

Restarting a node that has a masked column and a stored snapshot fails during 
startup:

{code}
java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
        at java.base/java.util.ArrayList.set(ArrayList.java:470)
        at 
org.apache.cassandra.cql3.functions.masking.ColumnMask$Serializer.deserialize(ColumnMask.java:318)
        at 
org.apache.cassandra.schema.ColumnMetadata$Serializer.deserialize(ColumnMetadata.java:819)
        at 
org.apache.cassandra.schema.TableMetadata$Serializer.deserialize(TableMetadata.java:2184)
        at 
org.apache.cassandra.schema.Tables$Serializer.deserialize(Tables.java:317)
        at 
org.apache.cassandra.schema.KeyspaceMetadata$Serializer.deserialize(KeyspaceMetadata.java:526)
        at 
org.apache.cassandra.schema.DistributedSchema$Serializer.deserialize(DistributedSchema.java:487)
        at 
org.apache.cassandra.tcm.ClusterMetadata$Serializer.deserialize(ClusterMetadata.java:1326)
        at 
org.apache.cassandra.tcm.MetadataSnapshots.fromBytes(MetadataSnapshots.java:64)
        at 
org.apache.cassandra.tcm.MetadataSnapshots$SystemKeyspaceMetadataSnapshots.getLatestSnapshot(MetadataSnapshots.java:131)
        at 
org.apache.cassandra.tcm.log.SystemKeyspaceStorage.getPersistedLogState(SystemKeyspaceStorage.java:133)
        at 
org.apache.cassandra.tcm.log.LocalLog.replayPersisted(LocalLog.java:584)
        at 
org.apache.cassandra.tcm.Startup.initializeAsNonCmsNode(Startup.java:185)
        at org.apache.cassandra.tcm.Startup.initialize(Startup.java:120)
{code}

Note that {{MetadataSnapshots.getSnapshot}} only catches {{IOException}}, so 
this unchecked
exception propagates rather than degrading to a null snapshot.

h2. Reproduction

{code}
CREATE TABLE ks.t (k int PRIMARY KEY, v text MASKED WITH mask_inner(2, 1));
nodetool cms snapshot
# restart the node -> startup fails with the stack above
{code}

h2. Affected versions

Present on {{cassandra-6.0}} and {{trunk}}. Not present on {{cassandra-5.0}}, 
which has no
{{ColumnMask.Serializer}}; dynamic data masking shipped in 5.0 but the schema 
is only serialized
into cluster metadata from 6.0 onwards. Patch is therefore based on 
{{cassandra-6.0}}.

h2. Tests

Existing DDM tests do not cover this because none of them force a metadata 
snapshot. Added:

* three round-trip cases in {{SchemaMetadataSerializationTest}} covering a 
masking function with no
partial arguments, with partial arguments, and with a null partial argument
* a new dtest {{ColumnMaskMetadataSnapshotTest}} covering snapshot read-back 
and node restart

All fail before the patch and pass after it.




--
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