Siyao Meng created HDDS-16914:
---------------------------------
Summary: SCM leader counts root CA rotation prepare acks per
certificate, so one SCM acking twice lets it commit before every SCM prepared
Key: HDDS-16914
URL: https://issues.apache.org/jira/browse/HDDS-16914
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments: MC-1-count-root-ca-rotation-prepare-acks-per-scm.patch,
TestBugMC1DuplicatePrepareAck.java
h3. Mechanism
{{RootCARotationHandlerImpl.rotationPrepareAck}} receives the new root
certificate ID, the sender's new sub-CA certificate ID and the sender's SCM ID.
It adds the certificate ID to a set and uses the SCM ID only in a log line.
{{RootCARotationManager.WaitSubCARotationPrepareAckTask}} submits
{{rotationCommit}} as soon as the size of that set equals the number of peers
in the Raft configuration.
An SCM that applies {{rotationPrepare}} a second time runs
{{SubCARotationPrepareTask}} again, which deletes its {{scm/sub-ca-next}}
directory, has a new sub-CA certificate signed with a new serial and sends
another ack. {{shouldSkipRootCert}} does not stop the second apply, because the
root certificate in memory only changes at {{rotationCommitted}}. An SCM
records a request as applied only when its transaction buffer is flushed
({{ozone.scm.ha.dbtransactionbuffer.flush.interval}}, default 60s), so an SCM
that is killed before the flush that follows {{rotationPrepare}} applies the
request again when it starts. An SCM that is killed in that interval after its
ack and is started again within {{hdds.x509.ca.rotation.ack.timeout}} (default
15 minutes) therefore acks twice. The leader of a three SCM ring then holds
three certificate IDs from two SCMs and commits although the third SCM has not
prepared.
h3. Trigger
Secure SCM HA with {{hdds.x509.ca.rotation.enabled=true}} (off by default),
three SCMs.
# One follower is down when the leader starts the rotation, so it does not ack.
# The other follower acks, is killed before its next transaction buffer flush
(default interval 60s) and is started again before the ack timeout (default 15
minutes). It applies {{rotationPrepare}} again and acks with a new certificate.
# The leader counts three acks and submits {{rotationCommit}} and
{{rotationCommitted}}.
Two overlapping faults are needed. Without the kill the same cluster behaves as
designed: the leader gets two acks, logs "Failed to receive all acks of
rotation prepare" at the timeout and does not commit.
h3. Impact
The rotation is committed while one SCM has no prepared sub-CA. On an
{{ozonesecure-ha}} compose cluster built from unmodified source (two runs) that
SCM, once started, applied {{rotationPrepare}} and {{rotationCommit}} within a
few milliseconds, before its prepare task had created {{scm/sub-ca-next}}: it
moved {{scm/sub-ca}} to {{scm/sub-ca-previous}}, failed the second move with
{{NoSuchFileException}}, and after a restart failed to start because
{{scm/sub-ca/certs}} was missing. The other two SCMs kept quorum and the leader
finished the rotation.
h3. Reproduction
PASS, deterministic, unmodified source, about 4 seconds.
[^TestBugMC1DuplicatePrepareAck.java] ({{hadoop-hdds/server-scm}}) runs one
SCM's rotation code in the test JVM: the real {{RootCARotationManager}} with
its tasks, {{RootCARotationHandlerImpl}}, {{SCMCertificateClient}} and the
certificate directories on disk. {{StorageContainerManager}} is a mock, the
Raft configuration is a mock that reports three peers, and a replicated request
is applied by calling the handler method the generated invoker calls. The first
test rebuilds the SCM objects from disk as after a restart, applies
{{rotationPrepare}} again and shows a second ack with the same SCM ID and a
different certificate ID. The second test lets the leader ack once, delivers
two acks from one follower and none from the third SCM, and shows the leader
logging "rotation is finished successfully" with the new root certificate in
memory. A passing test means the defect is present.
The kill and the Raft log replay are not part of this test. They were run on
the compose cluster described under Impact.
h3. Patch
[^MC-1-count-root-ca-rotation-prepare-acks-per-scm.patch], against
b9b33851b20fa11baf4b07982de9afb8ea44f85a. It also applies to master at
5108acff480.
The handler counts the SCM IDs of the senders instead of the certificate IDs.
The ack already carries the SCM ID, the tally only lives in the leader's
memory, and nothing changes in the replicated request, the generated invoker or
what is persisted. Counting per SCM means that every SCM has acked once, not
that every SCM is prepared at the moment of the commit: an SCM that is killed
after its ack and prepares again when it starts is still counted by its first
ack, which is a separate defect that this patch does not change (HDDS-16913).
Covered by {{testPrepareAckCountedOncePerScm}} in the existing
{{TestRootCARotationManager}}, which fails without the change (expected 2 acks,
was 3). With it {{TestRootCARotationManager}}, {{TestScmInvokerCodeGenerator}}
and {{TestScmCodecFactoryReplicateCoverage}} (16 tests) pass and checkstyle is
clean. With the patch the first test of the reproduction still passes, because
a second certificate is still signed, and the second fails because the leader
ends the rotation with "Failed to receive all acks of rotation prepare ...
received 2 acks". The patched build was not run on the compose cluster.
A stricter condition would be that the IDs of the acking SCMs equal the peer
IDs of the current Raft configuration (an SCM's Raft peer ID is its SCM ID). It
only differs when an SCM outside the configuration acks, and it needs a change
of the handler interface and its generated invoker, so the patch does not do it.
Found by TLA+ model checking and code review of the SCM root CA rotation paths
under HDDS-15926, on commit b9b33851b20fa11baf4b07982de9afb8ea44f85a. 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]