[ 
https://issues.apache.org/jira/browse/HDDS-16900?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Siyao Meng updated HDDS-16900:
------------------------------
    Attachment: TestBugMC2CertificateIdDivergence.java

> Primary SCM restart can lower the replicated CertificateId row, so 
> certificate IDs are returned twice
> -----------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-16900
>                 URL: https://issues.apache.org/jira/browse/HDDS-16900
>             Project: Apache Ozone
>          Issue Type: Bug
>            Reporter: Siyao Meng
>            Priority: Major
>         Attachments: 
> MC-1-do-not-lower-certificate-id-on-primary-scm-start.patch, 
> TestBugMC1CertificateIdReseed.java, TestBugMC2CertificateIdDivergence.java
>
>
> h3. Mechanism
> On every start of the primary SCM with security enabled, 
> {{StorageContainerManager.persistPrimarySCMCerts}} calls 
> {{SequenceIdGenerator.upgradeToCertificateSequenceId(scmMetadataStore, 
> true)}}. With {{force}} set, that method writes the largest serial found in 
> {{validSCMCertsTable}} and {{validCertsTable}} (at least 2) into the 
> {{CertificateId}} row of {{sequenceIdTable}}. It does not compare the new 
> value with the existing row (the row is only tested for absence, and with 
> {{force}} the result of that test is ignored), and it writes to the local 
> RocksDB directly, although once the row exists it is otherwise changed only 
> through Ratis ({{StateManagerImpl.allocateBatch}}).
> The row can be above the largest stored serial. 
> {{SCMSecurityProtocolServer.getEncodedCertToString}} allocates the ID in one 
> Ratis transaction before {{DefaultCAServer.signAndStoreCertificate}} signs 
> the certificate and stores it in a second one. If signing or storing fails, 
> the ID stays allocated and no certificate carries it. The row is also above 
> the largest stored serial after the certificate with that serial has expired 
> and been removed. On the next start of the primary the row is then set back, 
> the generator loads the lower value and returns IDs it has already returned.
> The other SCMs keep the replicated value, so the SCMs now disagree on a 
> replicated row. {{allocateBatch}} is a compare and set on the previous value, 
> so from then on an allocation changes the row on some SCMs and is a no-op on 
> the others, and every SCM that installs a checkpoint from the primary takes 
> over the lowered row.
> The forced call came with HDDS-9138. It is present in every release tag from 
> ozone-1.4.0 to ozone-2.2.1 (read from the tagged source, not run on a 
> release).
> h3. Trigger
> # The most recently allocated IDs have no stored certificate when the primary 
> stops, so the row is above the largest stored serial: signing or the store 
> transaction failed for the last request, or the certificate with the largest 
> serial expired and was removed.
> # The row is durable on the primary. A clean stop is enough: Ratis takes a 
> snapshot when the server stops, and {{SCMStateMachine.takeSnapshot}} flushes 
> the SCM transaction buffer.
> # The primary SCM is restarted.
> No setting has to be changed for this. If the SCM is killed before the 
> allocation was flushed, the allocation is replayed from the Raft log after 
> the forced write and the row is not lowered. The buffer is also flushed every 
> 60 seconds on a leader that is out of safe mode, when an SCM becomes leader 
> and at every Ratis snapshot, so a kill after one of those leaves the same 
> state as a clean stop (both read from the code, not run).
> h3. Impact
> Reproduced on unmodified source unless marked otherwise.
> * *An ID is returned twice (one SCM).* With IDs allocated up to 4 and 3 as 
> the largest stored serial, the row is 3 after a clean restart and the next 
> certificate gets serial 4. [^TestBugMC1CertificateIdReseed.java]
> * *Two certificates with the same serial (one SCM).* After the certificate 
> with serial 3 expired and was removed, the row is 2 after the restart and the 
> same CA issues a second certificate with serial 3 for another subject and 
> key. This needs the certificate with the largest serial to be expired and 
> removed. With the default lifetime of 365 days, renewal 28 days before 
> expiry, and removal only by {{ozone admin cert clean}} or by the root CA 
> rotation manager (off by default), that is not expected on a live cluster. 
> The test uses a lifetime of 3 seconds. [^TestBugMC1CertificateIdReseed.java]
> * *SCMs diverge and valid requests are refused (three SCMs, default 
> settings).* IDs were allocated up to 9 with 7 as the largest stored serial. 
> After its restart the primary has row 7 and the other two keep 9. Another SCM 
> leads and issues serials 10 and 11. On the primary the two allocations log 
> "Failed to allocate a batch for CertificateId, expected lastId is 9, actual 
> lastId is 7." and "expected lastId is 10, actual lastId is 7." and leave the 
> row at 7. When the primary leads next, five valid certificate requests in a 
> row end as: issued 8, issued 9, refused with "Conflicting certificate ID10", 
> refused with "Conflicting certificate ID11", issued 12. One request is 
> refused for every certificate the other leader issued in the meantime, so the 
> number of refusals to come grows for as long as an SCM with the higher value 
> leads. The allocation of 12 is the first that all three SCMs apply again 
> (read from the log of the run), so from there the rows are equal. A restart 
> of the lowered primary also raises its row, to the largest serial stored by 
> then, because the forced write runs again, on unpatched code too (read from 
> the code, not run). [^TestBugMC2CertificateIdDivergence.java]
> * *A new SCM starts with the lowered row (default settings).* A third SCM 
> that joins while the restarted primary leads installs the primary's 
> checkpoint. Its row is 7 while the remaining SCM has 9, and when it leads it 
> issues serial 8. [^TestBugMC2CertificateIdDivergence.java]
> * *An existing follower is lowered by a checkpoint (log purge enabled).* With 
> {{ozone.scm.ha.ratis.log.purge.enabled}} set and a small purge gap and 
> snapshot threshold, a follower that held row 9 durably and was down while the 
> primary restarted installs the primary's checkpoint. All three SCMs then 
> report the same applied index with rows 7, 7 and 9, and the follower issues 
> serial 8 when it leads. With log purge off (the default) an existing follower 
> catches up from the log and keeps its row (read from the Ratis code, not 
> run). [^TestBugMC2CertificateIdDivergence.java]
> In the multi SCM runs the IDs that were returned a second time had no stored 
> certificate, so no two stored certificates shared a serial. 
> {{SCMCertStore.checkValidCertID}} prevents that, and the same check is what 
> refuses the requests.
> h3. Reproduction
> The two attached classes are self contained and pass on unmodified source 
> when the bug is present (target path and Maven command are in each class 
> comment). Both go into {{hadoop-ozone/integration-test}} and use MiniKdc. No 
> state is injected and no source is changed.
> {{TestBugMC1CertificateIdReseed}} runs one secure SCM with default flush and 
> snapshot settings, requests certificates from the handler of 
> {{SCMSecurityProtocolServer}} (called in process) and stops and starts the 
> SCM. The second test shortens the certificate lifetime to 3 seconds.
> {{TestBugMC2CertificateIdDivergence}} runs a secure SCM HA mini cluster with 
> three SCMs per test and uses certificate requests, SCM stop and start, the 
> leadership transfer admin call and the checkpoint install that Ratis requests 
> by itself. The first and the last test use default Ratis settings. The second 
> enables log purge with purge gap 5 and snapshot threshold 5.
> In both classes the ID without a certificate is produced by the generator 
> call that {{getEncodedCertToString}} makes 
> ({{getNextId(SequenceIdType.CertificateId)}}), standing in for a request that 
> fails after the ID was taken. A failed request itself is not part of the 
> attached tests.
> h3. Patch
> [^MC-1-do-not-lower-certificate-id-on-primary-scm-start.patch], against 
> b0aa6475b7856d2c7f467dd3db12b9a1dd116f63. It also applies to master at 
> f650a22f3e3.
> {{upgradeToCertificateSequenceId}} now reads the existing row once and writes 
> only when there is no row or the largest stored serial is above it. The 
> forced call from {{persistPrimarySCMCerts}} stays and can still raise the row 
> over a certificate that was just stored, it can no longer lower it. On a 
> cluster initialised with the current code the forced call was already a no-op 
> at first start (the log shows "upgrade CertificateId to 2" twice), so nothing 
> changes for clusters the bug does not affect. Checkpoint install needs no 
> change: once the primary keeps its row, the checkpoint it serves carries the 
> replicated value.
> The patch keeps the local write to a replicated row, now raise only. Removing 
> the forced call (the first seeding of an absent row would stay local) or 
> sending it through Ratis is the stricter alternative. The patch also does not 
> repair rows that already differ. They converge as described under Impact.
> Covered by {{testForcedCertificateIdUpgradeKeepsAllocatedIds}} in the 
> existing {{TestSequenceIDGenerator}}, which fails without the change 
> ("expected: 4L but was: 3L"). With the patch {{TestSequenceIDGenerator}} and 
> seven surrounding SCM unit suites (68 tests), 
> {{TestSCMInstallSnapshotWithHA}} (3 tests) and five cases of 
> {{TestSecureOzoneCluster}} pass, and checkstyle is clean. The other four 
> cases of {{TestSecureOzoneCluster}} and {{TestSecretKeySnapshot}} time out in 
> this environment with and without the patch (Kerberos RPC connect timeouts) 
> and were left to CI. With the patch the scenarios of the attached classes 
> keep the row (4 on the single SCM, 9 on every SCM) and no request is refused, 
> so the attached classes, which assert the faulty values, fail at their first 
> assertion on the row.
> Found by TLA+ model checking and code review of the SCM sequence ID generator 
> and its startup and checkpoint paths under HDDS-15926, on commit 
> b0aa6475b7856d2c7f467dd3db12b9a1dd116f63. Checked against HDDS issues and 
> apache/ozone pull requests for duplicates before filing. Related but not 
> duplicates: HDDS-9138 (introduced the forced call), HDDS-16035 (a secure SCM 
> HA cluster cannot start because the primary refuses to sign certificates for 
> the other SCMs without a leader) and HDDS-14337 (seeding of the local ID in 
> the same class). 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]

Reply via email to