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

Siyao Meng updated HDDS-16464:
------------------------------
    Description: 
h3. Finding
Internal invariant violation (DeepCleanedImpliesDrained) that demonstrably 
propagates to an external effect: an unfenced deep-clean-flag write clobbers 
the purge-driven reset, so the snapshot is skipped by every future 
KeyDeletingService run and its now-reclaimable deleted keys are stranded for 
the snapshot's entire retention (space leak, understated exclusive-size 
accounting). Triggerable from ordinary key delete plus snapshot create/delete 
GC activity; harm is bounded (no data loss, no premature reclaim) and only 
escapes when the snapshot is itself deleted, so High rather than Critical.

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

h3. Reproduce
{noformat}
Ozone commit: 9fbf9ee0cb1bd2f5f5d437b6719ebbe5309351fb
Specula:      v1.1.0 (commit c6aa3dfa)
Target:       om-snapshot-gc
Guidance:     
campaigns/ozone-9fbf9ee/targets/008-om-snapshot-gc/.prompt-extra.md
{noformat}
{code:none}
specula run --agent=claude-code --effort=high --keep-original --max-parallel=2 \
  --enable-reviews --confirm-debate --tlc-memory-limit=28G --tlc-worker-limit=8 
\
  "om-snapshot-gc|apache/ozone|Java|Use the target-specific .prompt-extra.md"
{code}
Discovered under HDDS-15927 (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

1. KeyDeletingService processes a snapshot's deleted keys. Before scanning it 
captures the snapshot's previous snapshotId (expectedPreviousSnapshotId), which 
determines which keys are reclaimable relative to the previous snapshot.
2. It reclaims the eligible keys via a PurgeKeys request, which is fenced: it 
re-validates the previous snapshotId at apply time and skips if the chain 
changed.
3. In the same run it also submits a SetSnapshotPropertyRequest with 
deepCleanedDeletedKey=true to mark the snapshot fully cleaned. This request 
carried no previous-snapshot fence.
4. Between the scan and the apply of the deep-clean flag, the snapshot chain 
changes (a snapshot is created, deleted, or moved) so the snapshot's previous 
is no longer the one seen at scan time. The purge is correctly skipped by its 
fence, but the unfenced deep-clean flag is still applied.
5. The snapshot is now permanently marked deep cleaned while keys that should 
have been reclaimed against the changed previous remain. Deep clean never runs 
again for that snapshot, so those deleted keys are stranded and never reclaimed.

h3. Root cause

The key-deep-clean completion flag is written through 
SetSnapshotPropertyRequest.deepCleanedDeletedKey, but that request had no 
previous-snapshot fence, unlike the sibling PurgeKeys/PurgeDirectories requests 
which carry expectedPreviousSnapshotID and re-validate it at apply time 
(OMKeyPurgeRequest via SnapshotUtils.validatePreviousSnapshotId). 
KeyDeletingService already computes expectedPreviousSnapshotId at scan time but 
never propagated it to the deep-clean flag update, so the flag could be 
committed against a stale view of the chain.

h3. Suggested fix

Fence the deep-clean flag the same way the purge path is fenced. Add an 
optional NullableUUID expectedPreviousSnapshotID to SetSnapshotPropertyRequest 
(OmClientProtocol.proto), populate it in KeyDeletingService when submitting 
deepCleanedDeletedKey=true using the same expectedPreviousSnapshotId already 
captured at scan time, and re-validate it at apply time in 
OMSnapshotSetPropertyRequest.updateSnapshotProperty via 
SnapshotUtils.validatePreviousSnapshotId before calling setDeepClean(true). On 
a mismatch the request fails and is retried on the next run (fail-closed), so a 
stale deep-clean flag is never committed. The change is backward compatible: 
requests without the field (older senders) skip the check, matching the 
existing purge-path convention. A request-level unit test builds a two-snapshot 
chain and asserts that a deep-clean apply carrying a stale previous snapshotId 
is rejected and leaves the flag unset, while a matching one sets it. See 
[^HDDS-16464.001.patch].

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


  was:
h3. Finding
Internal invariant violation (DeepCleanedImpliesDrained) that demonstrably 
propagates to an external effect: an unfenced deep-clean-flag write clobbers 
the purge-driven reset, so the snapshot is skipped by every future 
KeyDeletingService run and its now-reclaimable deleted keys are stranded for 
the snapshot's entire retention (space leak, understated exclusive-size 
accounting). Triggerable from ordinary key delete plus snapshot create/delete 
GC activity; harm is bounded (no data loss, no premature reclaim) and only 
escapes when the snapshot is itself deleted, so High rather than Critical.

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

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

Generated with Specula (Claude Opus 4.8).


> Unfenced deep clean flag write strands a snapshot's reclaimable deleted keys 
> for its retention
> ----------------------------------------------------------------------------------------------
>
>                 Key: HDDS-16464
>                 URL: https://issues.apache.org/jira/browse/HDDS-16464
>             Project: Apache Ozone
>          Issue Type: Bug
>            Reporter: Siyao Meng
>            Priority: Major
>         Attachments: HDDS-16464.001.patch
>
>
> h3. Finding
> Internal invariant violation (DeepCleanedImpliesDrained) that demonstrably 
> propagates to an external effect: an unfenced deep-clean-flag write clobbers 
> the purge-driven reset, so the snapshot is skipped by every future 
> KeyDeletingService run and its now-reclaimable deleted keys are stranded for 
> the snapshot's entire retention (space leak, understated exclusive-size 
> accounting). Triggerable from ordinary key delete plus snapshot create/delete 
> GC activity; harm is bounded (no data loss, no premature reclaim) and only 
> escapes when the snapshot is itself deleted, so High rather than Critical.
> h3. Classification
> * Verdict: REPRODUCED
> * Severity: High
> * Source: Specula TLA+ model checking and confirmation debate, finding MC-1
> h3. Reproduce
> {noformat}
> Ozone commit: 9fbf9ee0cb1bd2f5f5d437b6719ebbe5309351fb
> Specula:      v1.1.0 (commit c6aa3dfa)
> Target:       om-snapshot-gc
> Guidance:     
> campaigns/ozone-9fbf9ee/targets/008-om-snapshot-gc/.prompt-extra.md
> {noformat}
> {code:none}
> specula run --agent=claude-code --effort=high --keep-original 
> --max-parallel=2 \
>   --enable-reviews --confirm-debate --tlc-memory-limit=28G 
> --tlc-worker-limit=8 \
>   "om-snapshot-gc|apache/ozone|Java|Use the target-specific .prompt-extra.md"
> {code}
> Discovered under HDDS-15927 (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
> 1. KeyDeletingService processes a snapshot's deleted keys. Before scanning it 
> captures the snapshot's previous snapshotId (expectedPreviousSnapshotId), 
> which determines which keys are reclaimable relative to the previous snapshot.
> 2. It reclaims the eligible keys via a PurgeKeys request, which is fenced: it 
> re-validates the previous snapshotId at apply time and skips if the chain 
> changed.
> 3. In the same run it also submits a SetSnapshotPropertyRequest with 
> deepCleanedDeletedKey=true to mark the snapshot fully cleaned. This request 
> carried no previous-snapshot fence.
> 4. Between the scan and the apply of the deep-clean flag, the snapshot chain 
> changes (a snapshot is created, deleted, or moved) so the snapshot's previous 
> is no longer the one seen at scan time. The purge is correctly skipped by its 
> fence, but the unfenced deep-clean flag is still applied.
> 5. The snapshot is now permanently marked deep cleaned while keys that should 
> have been reclaimed against the changed previous remain. Deep clean never 
> runs again for that snapshot, so those deleted keys are stranded and never 
> reclaimed.
> h3. Root cause
> The key-deep-clean completion flag is written through 
> SetSnapshotPropertyRequest.deepCleanedDeletedKey, but that request had no 
> previous-snapshot fence, unlike the sibling PurgeKeys/PurgeDirectories 
> requests which carry expectedPreviousSnapshotID and re-validate it at apply 
> time (OMKeyPurgeRequest via SnapshotUtils.validatePreviousSnapshotId). 
> KeyDeletingService already computes expectedPreviousSnapshotId at scan time 
> but never propagated it to the deep-clean flag update, so the flag could be 
> committed against a stale view of the chain.
> h3. Suggested fix
> Fence the deep-clean flag the same way the purge path is fenced. Add an 
> optional NullableUUID expectedPreviousSnapshotID to 
> SetSnapshotPropertyRequest (OmClientProtocol.proto), populate it in 
> KeyDeletingService when submitting deepCleanedDeletedKey=true using the same 
> expectedPreviousSnapshotId already captured at scan time, and re-validate it 
> at apply time in OMSnapshotSetPropertyRequest.updateSnapshotProperty via 
> SnapshotUtils.validatePreviousSnapshotId before calling setDeepClean(true). 
> On a mismatch the request fails and is retried on the next run (fail-closed), 
> so a stale deep-clean flag is never committed. The change is backward 
> compatible: requests without the field (older senders) skip the check, 
> matching the existing purge-path convention. A request-level unit test builds 
> a two-snapshot chain and asserts that a deep-clean apply carrying a stale 
> previous snapshotId is rejected and leaves the flag unset, while a matching 
> one sets it. See [^HDDS-16464.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