Siyao Meng created HDDS-17439:
---------------------------------
Summary: Datanode does not check that a PutBlock committed through
a data stream is for the stream's block and carries a valid block token
Key: HDDS-17439
URL: https://issues.apache.org/jira/browse/HDDS-17439
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments: CR-3-check-stream-putblock-block-and-token.patch
h3. Mechanism
When a client commits PutBlock through the data stream instead of the Raft log
({{ozone.client.datastream.putblock.without.raft.enabled}}, which opens the
stream with {{StreamInitWithPutBlock}}), the datanode itself executes two kinds
of PutBlock that it receives on the stream: a PutBlock sent as a stream command
in the middle of the stream ({{ContainerStateMachine.streamCommand}}, added by
HDDS-16008) and the PutBlock at the end of the stream, executed when the stream
is closed ({{KeyValueStreamDataChannel.closeWithStreamPutBlock}} calling
{{ContainerStateMachine.streamPutBlock}}, added by HDDS-15792). Both hand the
request to {{HddsDispatcher.dispatch}} with the dispatcher context ops
{{STREAM_COMMAND}} and {{STREAM_LINK}}.
A PutBlock sent through the Raft log has its block token checked by the leader
in {{ContainerStateMachine.startTransaction}}. On the two stream paths,
{{DispatcherContext.Op.validateToken}} returns false for {{STREAM_COMMAND}} and
{{STREAM_LINK}}, so {{HddsDispatcher.dispatch}} does not check the token of the
PutBlock. {{STREAM_LINK}} was added to that list by HDDS-9697, when it was used
for the PutBlock executed while applying the stream's Raft log entry, where a
second check of a token that was already checked could fail after the token
expired. Since HDDS-12007 and HDDS-15792 it is only used for the PutBlock
executed at stream close, which is not part of a log entry and is not replayed.
{{STREAM_COMMAND}} was added to the list by HDDS-16008.
Neither path compares the PutBlock with the block the stream was opened for.
The invariant the datanode should keep: a PutBlock received on a data stream is
for the stream's own block (the container ID and block ID of the stream's init
request) and carries a valid block token for that block, like every other
PutBlock.
h3. Trigger
Applies only when data streaming is enabled on the datanodes
({{hdds.container.ratis.datastream.enabled}}, default {{false}}) and the stream
is opened with {{StreamInitWithPutBlock}}, which the client does only with
{{ozone.client.datastream.putblock.without.raft.enabled}} (default {{false}}).
The stock {{BlockDataStreamOutput}} builds both PutBlocks for the stream's own
block with the stream's token ({{ContainerProtocolCalls.getPutBlockRequest}}),
so its writes meet the invariant and are unaffected by the change below.
h3. Impact
* On these two paths the datanode commits block metadata without the token
check that applies to every other PutBlock.
* A PutBlock at stream close that does not match the stream's block is still
committed, and the stream is then linked as if its own PutBlock had been
committed, so the stream reports success while its own block has no committed
metadata.
h3. Reproduction
No standalone reproduction is attached. The regression tests in the patch cover
the checks and fail on unmodified source:
*
{{ContainerStateMachineTests.testStreamCommandPutBlockNotMatchingStreamBlockIsRejected}}
({{hadoop-hdds/container-service}}, run by {{TestContainerStateMachineLeader}}
and {{TestContainerStateMachineFollower}}): a stream command whose PutBlock
does not match the stream's block must fail with {{MALFORMED_REQUEST}} and must
not reach the dispatcher. Without the change it fails with {{Expected
java.util.concurrent.ExecutionException to be thrown, but nothing was thrown.}}
*
{{ContainerStateMachineTests.testStreamClosePutBlockNotMatchingStreamBlockIsRejected}}:
the same for the PutBlock executed at stream close; a matching PutBlock is
still dispatched with {{STREAM_LINK}}. Without the change it fails with
{{Expected
org.apache.hadoop.hdds.scm.container.common.helpers.StorageContainerException
to be thrown, but nothing was thrown.}}
* {{TestHddsDispatcher.testValidateToken}}: {{STREAM_LINK}} and
{{STREAM_COMMAND}} move to the list of ops that check the token. Without the
change it fails with {{expected: <true> but was: <false>}}.
{{TestSecureContainerServer.testStreamPutBlocksWithTokenForStreamBlock}}
({{hadoop-ozone/integration-test}}) covers the compatible case on three secure
Ratis datanodes with data streaming: a stream opened with
{{StreamInitWithPutBlock}} whose PutBlocks are built as
{{BlockDataStreamOutput}} builds them (own block, own token) commits both the
mid stream and the close PutBlock. It passes with and without the change.
h3. Patch
[^CR-3-check-stream-putblock-block-and-token.patch], against
7212465c0bb19615708c144238317db51c2a696d with the patch of HDDS-17431 applied.
It applies on top of the patch of HDDS-17431 and depends on it: a PutBlock
rejected at stream close must fail the stream close, which HDDS-17431 adds.
With the patch of HDDS-17431 applied first, it also applies to master at
1acf315d546. The patch of HDDS-17438 also edits the stream test setup in
{{ContainerStateMachineTests}}; whichever lands second needs a small rebase
there.
* {{DispatcherContext.Op.validateToken}} now returns true for {{STREAM_LINK}}
and {{STREAM_COMMAND}}, so {{HddsDispatcher.dispatch}} checks the PutBlock's
own token on both stream paths. Neither op is used while applying or replaying
a Raft log entry any more, so this does not bring back the double check that
HDDS-9697 removed; {{APPLY_TRANSACTION}}, {{WRITE_STATE_MACHINE_DATA}} and
{{READ_STATE_MACHINE_DATA}} keep returning false.
* {{ContainerStateMachine}} keeps the stream's init request in the two
callbacks it creates for the stream, and a new method, {{checkStreamPutBlock}},
checks that each PutBlock is for the stream's container and block before it is
dispatched. A mismatch fails the command or the stream close with
{{MALFORMED_REQUEST}}.
Both checks are needed: the token check verifies the PutBlock's own token, and
the block check ties the PutBlock to the stream. There is no wire or protocol
change and no new configuration. With the patch {{TestHddsDispatcher}},
{{TestContainerStateMachineLeader}}, {{TestContainerStateMachineFollower}},
{{TestKeyValueStreamDataChannel}} and {{TestKeyValueHandler}} pass (94 tests),
{{TestBlockDataStreamOutput}} in {{hadoop-ozone/integration-test}} (mini
cluster, with and without PutBlock through the data stream, 20 tests) and
{{TestSecureContainerServer}} (3 tests) pass, and checkstyle on
{{hadoop-hdds/container-service}} and {{hadoop-ozone/integration-test}} is
clean.
Found by code review of the datanode streaming PutBlock paths, as part of the
TLA+ verification effort under HDDS-15926, on commit
7212465c0bb19615708c144238317db51c2a696d. Checked against HDDS issues and
apache/ozone pull requests for duplicates before filing. Related but not
duplicates: HDDS-9697 (removed the token check from the state machine paths
other than {{startTransaction}}), HDDS-16008 (added the stream command),
HDDS-15792 (added the PutBlock at stream close), HDDS-16828 (finalized block
check on the same paths). The attached patch is a proposal for review.
Generated with Specula (Claude Opus 5.5).
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]