Siyao Meng created HDDS-16805:
---------------------------------
Summary: Container reconciliation does not verify chunks pulled
from a peer when ozone.client.verify.checksum is false on the datanode
Key: HDDS-16805
URL: https://issues.apache.org/jira/browse/HDDS-16805
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments: MC-7-verify-peer-chunk-checksums-in-reconciliation.patch,
TestBugMC7ReconcileUnverifiedPeerChunk.java
h3. Mechanism
{{KeyValueHandler.reconcileChunksPerBlock}} reads chunks from a peer through a
{{BlockInputStream}} that it creates with
{{conf.getObject(OzoneClientConfig.class)}}, the datanode's own view of the
client configuration. The client streams verify what they read only when
{{ozone.client.verify.checksum}} is true, and the peer does not verify on
{{ReadChunk}}. With the setting false in the datanode's configuration, a chunk
that is corrupt on the peer is written into the local replica, the local merkle
tree records it as healthy with the checksum from the peer's block metadata,
and the block is committed with the peer's BCSID. A chunk that the local tree
lists as healthy is never pulled, so this affects blocks and chunks that the
replica lacks or that its own scanner marked corrupt.
The peer's tree is only as fresh as the peer's last scan, so a chunk that went
bad since then is still listed as healthy and offered for repair.
The reconciliation design document says that the datanode validates the
checksum of a chunk read from a peer before using it, and that the existing
client code already does this. That holds only while the client setting is on.
{{ECReconstructionCoordinator}}, the other datanode path that reads peer data
through the client streams, calls {{setChecksumVerify(true)}} on its config
before it opens the stream. The line in {{reconcileChunksPerBlock}} dates from
HDDS-11763 (read from history).
h3. Trigger
# {{ozone.client.verify.checksum=false}} is in the configuration that the
datanode loads, for example a site file shared with clients.
# A replica lacks a block or chunk that a peer has, or holds a chunk that its
own scanner marked corrupt.
# On the peer that chunk is corrupt on disk and the peer has not been scanned
since.
# An administrator runs {{ozone admin container reconcile}} for the container.
h3. Impact
* The corrupt chunk is copied into the reconciling replica and committed. A
later round against a healthy peer in the same reconcile task does not repair
it, because the local tree already lists the chunk as healthy: after
reconciling with the corrupt peer and then a healthy one, the block still holds
the corrupt bytes (run).
* The on demand scan at the end of the reconcile task finds the mismatch and
marks the replica UNHEALTHY. In the reproduction a replica that was CLOSED and
only lacked one block before the reconcile is UNHEALTHY after it (run).
* With the default setting the corrupt chunk is rejected with a checksum
mismatch and the block is repaired from the healthy peer (run, as the control).
h3. Reproduction
PASS, deterministic, unmodified source.
[^TestBugMC7ReconcileUnverifiedPeerChunk.java]
({{hadoop-hdds/container-service}}) uses the setup of
{{TestContainerReconciliationWithMockDatanodes}}: three datanodes, each a real
{{KeyValueHandler}} with a real container on disk and a real on demand scanner,
the real {{BlockInputStream}}, and only the static {{ContainerProtocolCalls}}
transport routed to the peer's handler. There is no SCM. One replica loses a
block, one peer has a byte flipped in that block and is not scanned, the other
peer is healthy. The replica reconciles with the corrupt peer first and the
healthy peer second. With the setting false on the reconciling datanode the
block is committed with the peer's BCSID, the block file equals the corrupt
peer's bytes, and the replica is UNHEALTHY after the scan. With the setting
true the block file equals the healthy bytes and the replica stays CLOSED. A
passing test means the defect is present.
h3. Patch
[^MC-7-verify-peer-chunk-checksums-in-reconciliation.patch], against
1e528b97aed3eef1be6846f1f0767c1ef98a883a. It also applies to master at
8ca44914771.
{{reconcileChunksPerBlock}} calls {{setChecksumVerify(true)}} on the
{{OzoneClientConfig}} that it passes to the stream, as
{{ECReconstructionCoordinator}} does. {{getObject}} returns a new object on
every call, so no other code sees the flag. Nothing changes with the default
setting. With the setting false, reconciliation now computes checksums over
what it pulls, and a chunk that fails ends the repair of that block from that
peer, which is the existing handling of a chunk that cannot be pulled. The next
peer is then tried.
Covered by
{{testContainerReconciliationVerifiesPeerDataWhenClientChecksumVerifyDisabled}}
in the existing {{TestContainerReconciliationWithMockDatanodes}}. Without the
change it fails because the data checksum of the replica after reconcile and
scan is not the healthy one. With it the unit suites of the {{keyvalue}},
{{ozoneimpl}} and {{checksum}} packages in {{hadoop-hdds/container-service}}
pass (1124 tests, 18 skipped by the suites), checkstyle is clean, and the case
of the reproduction with the setting false fails because the block now holds
the healthy bytes. Not run on a mini cluster.
Found by TLA+ model checking and code review of the container reconciliation
paths in the datanode and SCM under HDDS-15926, on commit
1e528b97aed3eef1be6846f1f0767c1ef98a883a. 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]