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

Siyao Meng updated HDDS-16451:
------------------------------
    Description: 
h3. Finding
Cross-lock lost update of a shared ContainerInfo sequenceId (report path under 
synchronized(containerInfo), Ratis-apply path under striped writeLock(id)) is a 
real race but its harm is masked: for OPEN/QUASI_CLOSED the periodic report 
resend re-raises the value via max(), and the durable/follower value is 
serialized under writeLock so an in-memory clobber cannot corrupt it. Internal 
invariant violation with downstream risk (an understated in-memory seqId 
feeding a resurrection/close decision in a narrow window) but no consumer 
observed the wrong outcome.

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

h3. Reproduce
{noformat}
Ozone commit: 9fbf9ee0cb1bd2f5f5d437b6719ebbe5309351fb
Specula:      v1.1.0 (commit c6aa3dfa)
Target:       scm-container-lifecycle
Guidance:     
campaigns/ozone-9fbf9ee/targets/015-scm-container-lifecycle/.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-container-lifecycle|apache/ozone|Java|Use the target-specific 
.prompt-extra.md"
{code}
Discovered under HDDS-16431 (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 lost update is real but its harm is 
masked for the common cases; a lasting wrong value needs the unmasking window 
below.

1. On the leader SCM a report-processing thread runs 
AbstractContainerReportHandler.processContainerReplica under 
synchronized(containerInfo) and calls 
containerInfo.updateSequenceId(replicaBcsid).
2. Concurrently the Ratis state-machine apply thread runs 
ContainerStateManagerImpl.updateContainerStateWithSequenceId under the striped 
writeLock(id) and calls containerInfo.updateSequenceId(sequenceId) on the same 
ContainerInfo instance.
3. updateSequenceId is a non-atomic read-modify-write (sequenceId = max(param, 
sequenceId)) on a non-volatile long. The two lock domains (object monitor vs 
striped read-write lock) do not exclude each other, so the two updates 
interleave: one thread reads the old value, the other writes the higher value, 
and the first thread then writes back its lower max, clobbering the higher 
value.

Unmasking window: for OPEN / QUASI_CLOSED containers the periodic 
full/incremental container report resend re-invokes updateSequenceId and 
re-raises the value via max(), and the durable/follower value is written under 
the writeLock, so an in-memory clobber self-corrects. The residual un-masked 
case (not injectable in a plain unit test) is a CLOSED container that is frozen 
at a clobbered sequenceId at close time, which then feeds a resurrection/close 
or replica-deletion decision keyed on that value.

h3. Root cause

hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerInfo.java:
 updateSequenceId performs a plain read-modify-write on a non-volatile long 
field, while the same instance is updated under two different locks (a 
documented TODO in AbstractContainerReportHandler notes the lock discipline is 
not unified). The RMW is therefore neither atomic nor guaranteed visible across 
threads, so a concurrent higher value can be lost.

h3. Suggested fix

Make the max update atomic without introducing a new lock. Mark sequenceId 
volatile (for cross-thread read visibility) and perform the read-modify-write 
through a lock-free AtomicLongFieldUpdater.accumulateAndGet(this, sequenceID, 
Math::max), which retries via compare-and-set until the stored value is the 
maximum. A lock-free CAS is used rather than synchronizing updateSequenceId on 
the ContainerInfo monitor, because the report path already holds that monitor 
and can acquire writeLock(id) beneath it while the apply path acquires 
writeLock(id) first, so taking the monitor inside updateSequenceId could 
deadlock. A concurrency invariant test asserts that after many concurrent 
updates the stored value equals the maximum of all inputs. See 
[^HDDS-16451.001.patch].

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


  was:
h3. Finding
Cross-lock lost update of a shared ContainerInfo sequenceId (report path under 
synchronized(containerInfo), Ratis-apply path under striped writeLock(id)) is a 
real race but its harm is masked: for OPEN/QUASI_CLOSED the periodic report 
resend re-raises the value via max(), and the durable/follower value is 
serialized under writeLock so an in-memory clobber cannot corrupt it. Internal 
invariant violation with downstream risk (an understated in-memory seqId 
feeding a resurrection/close decision in a narrow window) but no consumer 
observed the wrong outcome.

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

h3. Reproduce
{noformat}
Ozone commit: 9fbf9ee0cb1bd2f5f5d437b6719ebbe5309351fb
Specula:      v1.1.0 (commit c6aa3dfa)
Target:       scm-container-lifecycle
Guidance:     
campaigns/ozone-9fbf9ee/targets/015-scm-container-lifecycle/.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-container-lifecycle|apache/ozone|Java|Use the target-specific 
.prompt-extra.md"
{code}
Discovered under HDDS-16431 (Specula TLA+ verification effort). The TLA+ 
specification, counterexample, and confirmation debate live in the Specula run 
artifacts.

Generated with Specula (Claude Opus 4.8).


> Cross lock lost update of ContainerInfo sequenceId (masked by report resend 
> and writeLock)
> ------------------------------------------------------------------------------------------
>
>                 Key: HDDS-16451
>                 URL: https://issues.apache.org/jira/browse/HDDS-16451
>             Project: Apache Ozone
>          Issue Type: Bug
>            Reporter: Siyao Meng
>            Priority: Minor
>         Attachments: HDDS-16451.001.patch
>
>
> h3. Finding
> Cross-lock lost update of a shared ContainerInfo sequenceId (report path 
> under synchronized(containerInfo), Ratis-apply path under striped 
> writeLock(id)) is a real race but its harm is masked: for OPEN/QUASI_CLOSED 
> the periodic report resend re-raises the value via max(), and the 
> durable/follower value is serialized under writeLock so an in-memory clobber 
> cannot corrupt it. Internal invariant violation with downstream risk (an 
> understated in-memory seqId feeding a resurrection/close decision in a narrow 
> window) but no consumer observed the wrong outcome.
> h3. Classification
> * Verdict: MASKED
> * Severity: Medium
> * Source: Specula TLA+ model checking and confirmation debate, finding CR-4
> h3. Reproduce
> {noformat}
> Ozone commit: 9fbf9ee0cb1bd2f5f5d437b6719ebbe5309351fb
> Specula:      v1.1.0 (commit c6aa3dfa)
> Target:       scm-container-lifecycle
> Guidance:     
> campaigns/ozone-9fbf9ee/targets/015-scm-container-lifecycle/.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-container-lifecycle|apache/ozone|Java|Use the target-specific 
> .prompt-extra.md"
> {code}
> Discovered under HDDS-16431 (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 lost update is real but its harm is 
> masked for the common cases; a lasting wrong value needs the unmasking window 
> below.
> 1. On the leader SCM a report-processing thread runs 
> AbstractContainerReportHandler.processContainerReplica under 
> synchronized(containerInfo) and calls 
> containerInfo.updateSequenceId(replicaBcsid).
> 2. Concurrently the Ratis state-machine apply thread runs 
> ContainerStateManagerImpl.updateContainerStateWithSequenceId under the 
> striped writeLock(id) and calls containerInfo.updateSequenceId(sequenceId) on 
> the same ContainerInfo instance.
> 3. updateSequenceId is a non-atomic read-modify-write (sequenceId = 
> max(param, sequenceId)) on a non-volatile long. The two lock domains (object 
> monitor vs striped read-write lock) do not exclude each other, so the two 
> updates interleave: one thread reads the old value, the other writes the 
> higher value, and the first thread then writes back its lower max, clobbering 
> the higher value.
> Unmasking window: for OPEN / QUASI_CLOSED containers the periodic 
> full/incremental container report resend re-invokes updateSequenceId and 
> re-raises the value via max(), and the durable/follower value is written 
> under the writeLock, so an in-memory clobber self-corrects. The residual 
> un-masked case (not injectable in a plain unit test) is a CLOSED container 
> that is frozen at a clobbered sequenceId at close time, which then feeds a 
> resurrection/close or replica-deletion decision keyed on that value.
> h3. Root cause
> hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerInfo.java:
>  updateSequenceId performs a plain read-modify-write on a non-volatile long 
> field, while the same instance is updated under two different locks (a 
> documented TODO in AbstractContainerReportHandler notes the lock discipline 
> is not unified). The RMW is therefore neither atomic nor guaranteed visible 
> across threads, so a concurrent higher value can be lost.
> h3. Suggested fix
> Make the max update atomic without introducing a new lock. Mark sequenceId 
> volatile (for cross-thread read visibility) and perform the read-modify-write 
> through a lock-free AtomicLongFieldUpdater.accumulateAndGet(this, sequenceID, 
> Math::max), which retries via compare-and-set until the stored value is the 
> maximum. A lock-free CAS is used rather than synchronizing updateSequenceId 
> on the ContainerInfo monitor, because the report path already holds that 
> monitor and can acquire writeLock(id) beneath it while the apply path 
> acquires writeLock(id) first, so taking the monitor inside updateSequenceId 
> could deadlock. A concurrency invariant test asserts that after many 
> concurrent updates the stored value equals the maximum of all inputs. See 
> [^HDDS-16451.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