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

Siyao Meng updated HDDS-16453:
------------------------------
    Description: 
h3. Finding
An unguarded flush() during an apply commits data under a stale transaction 
index (DurableIndexMatchesData violated), so a crash+recover re-applies an 
already-durable entry. Reachable from the real notifyLeaderChanged / 
saveConfiguration flush callers, but the consequence is masked by SCM's 
idempotent-replay design (all @Replicate transactions are absolute-put/CAS; the 
one non-idempotent quantity is a deprecated no-op). Internal invariant 
violation with no external effect today but real downstream divergence risk the 
moment a non-idempotent replicated transaction is added.

h3. Classification
* Verdict: MASKED
* Severity: Medium
* Source: Specula TLA+ model checking and confirmation debate, finding MC-1

h3. Reproduce
{noformat}
Ozone commit: 9fbf9ee0cb1bd2f5f5d437b6719ebbe5309351fb
Specula:      v1.1.0 (commit c6aa3dfa)
Target:       scm-ha-tx-buffer
Guidance:     
campaigns/ozone-9fbf9ee/targets/018-scm-ha-tx-buffer/.prompt-extra.md
{noformat}
{code:none}
specula run --agent=claude-code --effort=medium --keep-original 
--max-parallel=2 \
  --enable-reviews --confirm-debate --tlc-memory-limit=28G --tlc-worker-limit=8 
\
  "scm-ha-tx-buffer|apache/ozone|Java|Use the target-specific .prompt-extra.md"
{code}
Discovered under HDDS-16432 (Specula TLA+ verification effort). The TLA+ 
specification, counterexample, and confirmation debate live in the Specula run 
artifacts.

Generated with Specula (Claude Opus 4.8).

h3. Steps to reproduce

This is a latent (masked) defect. The invariant DurableIndexMatchesData is 
violated in memory and on disk, but current SCM state converges on replay; a 
full end-to-end divergence needs the unmasking condition below.

Deterministic site (saveConfiguration): 
StatefulServiceStateManagerImpl.saveConfiguration runs inside the apply of its 
own @Replicate call.

1. saveConfiguration stages the config bytes via addToBuffer, then calls the 
unguarded SCMHADBTransactionBuffer.flush(). The outer applyTransaction has not 
yet called updateLatestTrxInfo, so the durable index is still i-1.
2. flush() commits the batch atomically as {config data, index i-1}. The config 
is durable under a stale index.

Concurrent site (notifyLeaderChanged): the leader-change callback calls the 
unguarded flush() while an applyTransaction may be in flight (data staged for 
index i, index not yet advanced), committing that transaction's data under 
index i-1.

Unmasking condition (not reproducible in a plain unit test): a crash after such 
a stale-index flush but before the index is persisted causes recovery to read 
index i-1 and replay entry i, which is already durable. For every current SCM 
transaction the replay is idempotent (absolute-put / CAS), so the state 
converges and the harm is masked. If a non-idempotent replicated transaction 
were added, the re-apply would diverge across SCM replicas with no automatic 
recovery.

h3. Root cause

Two callers use the unguarded SCMHADBTransactionBuffer.flush(), which commits 
the current batch under the current (not yet advanced) transaction index. 
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/ha/StatefulServiceStateManagerImpl.java
 (saveConfiguration) and 
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/ha/SCMStateMachine.java
 (notifyLeaderChanged). flush() takes only the write lock; unlike flushIfNeeded 
it does not check applyingTransactions, so it is not deferred while a 
transaction is being applied. HDDS-15065 added the applyingTransactions guard 
and moved the periodic/monitor flush onto it, but these two direct-flush 
callers were left unguarded.

h3. Suggested fix

Route both callers through the existing guarded flushIfNeeded(0) instead of 
flush(). While a transaction apply is in progress it defers, so the staged data 
is committed with a consistent index by the next flush; when no apply is in 
progress it flushes pending data as before. This reuses the same guard 
HDDS-15065 established. A unit test constructs StatefulServiceStateManagerImpl 
with the real transaction buffer and asserts that saveConfiguration during an 
apply window does not commit config under the stale index (the assertion fails 
on the old flush() and passes with the fix); notifyLeaderChanged is the 
concurrent variant fixed the same way. See [^HDDS-16453.001.patch].

Patch is an AI-drafted proposal (Specula + Claude); pending human build, full 
test, and review before merge.


  was:
h3. Finding
An unguarded flush() during an apply commits data under a stale transaction 
index (DurableIndexMatchesData violated), so a crash+recover re-applies an 
already-durable entry. Reachable from the real notifyLeaderChanged / 
saveConfiguration flush callers, but the consequence is masked by SCM's 
idempotent-replay design (all @Replicate transactions are absolute-put/CAS; the 
one non-idempotent quantity is a deprecated no-op). Internal invariant 
violation with no external effect today but real downstream divergence risk the 
moment a non-idempotent replicated transaction is added.

h3. Classification
* Verdict: MASKED
* Severity: Medium
* Source: Specula TLA+ model checking and confirmation debate, finding MC-1

h3. Reproduce
{noformat}
Ozone commit: 9fbf9ee0cb1bd2f5f5d437b6719ebbe5309351fb
Specula:      v1.1.0 (commit c6aa3dfa)
Target:       scm-ha-tx-buffer
Guidance:     
campaigns/ozone-9fbf9ee/targets/018-scm-ha-tx-buffer/.prompt-extra.md
{noformat}
{code:none}
specula run --agent=claude-code --effort=medium --keep-original 
--max-parallel=2 \
  --enable-reviews --confirm-debate --tlc-memory-limit=28G --tlc-worker-limit=8 
\
  "scm-ha-tx-buffer|apache/ozone|Java|Use the target-specific .prompt-extra.md"
{code}
Discovered under HDDS-16432 (Specula TLA+ verification effort). The TLA+ 
specification, counterexample, and confirmation debate live in the Specula run 
artifacts.

Generated with Specula (Claude Opus 4.8).


> Unguarded transaction buffer flush commits data under a stale index (masked 
> by idempotent replay)
> -------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-16453
>                 URL: https://issues.apache.org/jira/browse/HDDS-16453
>             Project: Apache Ozone
>          Issue Type: Bug
>            Reporter: Siyao Meng
>            Priority: Minor
>         Attachments: HDDS-16453.001.patch
>
>
> h3. Finding
> An unguarded flush() during an apply commits data under a stale transaction 
> index (DurableIndexMatchesData violated), so a crash+recover re-applies an 
> already-durable entry. Reachable from the real notifyLeaderChanged / 
> saveConfiguration flush callers, but the consequence is masked by SCM's 
> idempotent-replay design (all @Replicate transactions are absolute-put/CAS; 
> the one non-idempotent quantity is a deprecated no-op). Internal invariant 
> violation with no external effect today but real downstream divergence risk 
> the moment a non-idempotent replicated transaction is added.
> h3. Classification
> * Verdict: MASKED
> * Severity: Medium
> * Source: Specula TLA+ model checking and confirmation debate, finding MC-1
> h3. Reproduce
> {noformat}
> Ozone commit: 9fbf9ee0cb1bd2f5f5d437b6719ebbe5309351fb
> Specula:      v1.1.0 (commit c6aa3dfa)
> Target:       scm-ha-tx-buffer
> Guidance:     
> campaigns/ozone-9fbf9ee/targets/018-scm-ha-tx-buffer/.prompt-extra.md
> {noformat}
> {code:none}
> specula run --agent=claude-code --effort=medium --keep-original 
> --max-parallel=2 \
>   --enable-reviews --confirm-debate --tlc-memory-limit=28G 
> --tlc-worker-limit=8 \
>   "scm-ha-tx-buffer|apache/ozone|Java|Use the target-specific 
> .prompt-extra.md"
> {code}
> Discovered under HDDS-16432 (Specula TLA+ verification effort). The TLA+ 
> specification, counterexample, and confirmation debate live in the Specula 
> run artifacts.
> Generated with Specula (Claude Opus 4.8).
> h3. Steps to reproduce
> This is a latent (masked) defect. The invariant DurableIndexMatchesData is 
> violated in memory and on disk, but current SCM state converges on replay; a 
> full end-to-end divergence needs the unmasking condition below.
> Deterministic site (saveConfiguration): 
> StatefulServiceStateManagerImpl.saveConfiguration runs inside the apply of 
> its own @Replicate call.
> 1. saveConfiguration stages the config bytes via addToBuffer, then calls the 
> unguarded SCMHADBTransactionBuffer.flush(). The outer applyTransaction has 
> not yet called updateLatestTrxInfo, so the durable index is still i-1.
> 2. flush() commits the batch atomically as {config data, index i-1}. The 
> config is durable under a stale index.
> Concurrent site (notifyLeaderChanged): the leader-change callback calls the 
> unguarded flush() while an applyTransaction may be in flight (data staged for 
> index i, index not yet advanced), committing that transaction's data under 
> index i-1.
> Unmasking condition (not reproducible in a plain unit test): a crash after 
> such a stale-index flush but before the index is persisted causes recovery to 
> read index i-1 and replay entry i, which is already durable. For every 
> current SCM transaction the replay is idempotent (absolute-put / CAS), so the 
> state converges and the harm is masked. If a non-idempotent replicated 
> transaction were added, the re-apply would diverge across SCM replicas with 
> no automatic recovery.
> h3. Root cause
> Two callers use the unguarded SCMHADBTransactionBuffer.flush(), which commits 
> the current batch under the current (not yet advanced) transaction index. 
> hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/ha/StatefulServiceStateManagerImpl.java
>  (saveConfiguration) and 
> hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/ha/SCMStateMachine.java
>  (notifyLeaderChanged). flush() takes only the write lock; unlike 
> flushIfNeeded it does not check applyingTransactions, so it is not deferred 
> while a transaction is being applied. HDDS-15065 added the 
> applyingTransactions guard and moved the periodic/monitor flush onto it, but 
> these two direct-flush callers were left unguarded.
> h3. Suggested fix
> Route both callers through the existing guarded flushIfNeeded(0) instead of 
> flush(). While a transaction apply is in progress it defers, so the staged 
> data is committed with a consistent index by the next flush; when no apply is 
> in progress it flushes pending data as before. This reuses the same guard 
> HDDS-15065 established. A unit test constructs 
> StatefulServiceStateManagerImpl with the real transaction buffer and asserts 
> that saveConfiguration during an apply window does not commit config under 
> the stale index (the assertion fails on the old flush() and passes with the 
> fix); notifyLeaderChanged is the concurrent variant fixed the same way. See 
> [^HDDS-16453.001.patch].
> Patch is an AI-drafted proposal (Specula + Claude); pending human build, full 
> test, and review before merge.



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