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

Siyao Meng updated HDDS-16465:
------------------------------
    Description: 
h3. Finding
If the named masks were absent, the non-atomic dual-chain update / restart 
rebuild could give a surviving snapshot a different effective previous and 
misdirect the adjacency reclaim filter, physically reclaiming a still 
chain-visible key: externally observable data loss on snapshot reads with no 
automatic recovery. Reachable in principle from concurrent purge plus restart 
during snapshot GC. Currently masked by atomic single-batch purge persistence, 
the fail-closed corruption gate on restart rebuild, and reclaim-time 
re-validation (validatePreviousSnapshotId / 
validateExistingLastNSnapshotsInChain under SNAPSHOT_GC_LOCK plus the flush 
gate).

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

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. SnapshotChainManager keeps two in-memory chains that must agree: the global 
chain (all snapshots in creation order) and the per-path chain (snapshots for a 
given volume/bucket). GC and reclaim navigate them via previous/next-snapshot 
lookups.
2. A snapshot delete calls deleteSnapshot, which ran deleteSnapshotGlobal(id) 
&& deleteSnapshotPath(path, id). Because of the short-circuit &&, if the global 
delete commits (returns true) but the path delete then returns false or throws, 
the global chain is mutated while the path chain is not. If the snapshot is 
missing from the global chain, the path delete is never attempted at all.
3. Either way the two chains diverge: one chain no longer contains the snapshot 
while the other still links through it. Subsequent previous/next-snapshot 
navigation returns inconsistent results, which can misdirect reclaim decisions.
4. addSnapshot had the mirror-image gap: it committed the global insertion 
before attempting the path insertion, so a failing path insertion left the 
snapshot stranded in the global chain.
5. Independently, latestGlobalSnapshotId and oldestGlobalSnapshotId were 
written only under the instance monitor but read by 
getLatestGlobalSnapshotId/getOldestGlobalSnapshotId (and the chain iterator) 
without holding it, so a concurrent reader could observe a stale value.

h3. Root cause

The dual-chain updates in addSnapshot and deleteSnapshot were not atomic and 
had no rollback, and deleteSnapshot's short-circuit && could skip or half-apply 
an update, so a failure or absence in one chain left the two chains 
inconsistent. The two global head/tail fields were also published without the 
memory visibility guarantee their unsynchronized getters rely on.

h3. Suggested fix

Make both chain mutations all-or-nothing. deleteSnapshot now checks membership 
in both chains first and refuses a partial delete (throws) when a snapshot is 
present in only one of them, and rolls back the global deletion 
(restoreSnapshotGlobal) if the path deletion fails. addSnapshot rolls back the 
global insertion if the path insertion fails. latestGlobalSnapshotId and 
oldestGlobalSnapshotId are made volatile so the unsynchronized getters observe 
the last committed value. Two invariant unit tests are added: a failing path 
insertion leaves both chains empty (add rollback), and a delete whose path does 
not match is refused and leaves the global chain unchanged. See 
[^HDDS-16465.001.patch].

Note: the underlying trigger is a concurrency window (a failing or asymmetric 
chain update, or a reader racing a writer) that is timing dependent and not 
deterministically reproducible without a thread-scheduling seam that shipping 
code does not expose. The added tests pin the atomicity and partial-update 
invariants directly rather than reproducing a specific interleaving; the 
memory-visibility change is asserted by inspection.

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


  was:
h3. Finding
If the named masks were absent, the non-atomic dual-chain update / restart 
rebuild could give a surviving snapshot a different effective previous and 
misdirect the adjacency reclaim filter, physically reclaiming a still 
chain-visible key: externally observable data loss on snapshot reads with no 
automatic recovery. Reachable in principle from concurrent purge plus restart 
during snapshot GC. Currently masked by atomic single-batch purge persistence, 
the fail-closed corruption gate on restart rebuild, and reclaim-time 
re-validation (validatePreviousSnapshotId / 
validateExistingLastNSnapshotsInChain under SNAPSHOT_GC_LOCK plus the flush 
gate).

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

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


> Non atomic snapshot chain update could misdirect the adjacency reclaim filter
> -----------------------------------------------------------------------------
>
>                 Key: HDDS-16465
>                 URL: https://issues.apache.org/jira/browse/HDDS-16465
>             Project: Apache Ozone
>          Issue Type: Bug
>            Reporter: Siyao Meng
>            Priority: Critical
>         Attachments: HDDS-16465.001.patch
>
>
> h3. Finding
> If the named masks were absent, the non-atomic dual-chain update / restart 
> rebuild could give a surviving snapshot a different effective previous and 
> misdirect the adjacency reclaim filter, physically reclaiming a still 
> chain-visible key: externally observable data loss on snapshot reads with no 
> automatic recovery. Reachable in principle from concurrent purge plus restart 
> during snapshot GC. Currently masked by atomic single-batch purge 
> persistence, the fail-closed corruption gate on restart rebuild, and 
> reclaim-time re-validation (validatePreviousSnapshotId / 
> validateExistingLastNSnapshotsInChain under SNAPSHOT_GC_LOCK plus the flush 
> gate).
> h3. Classification
> * Verdict: MASKED
> * Severity: Critical
> * Source: Specula TLA+ model checking and confirmation debate, finding CR-4
> 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. SnapshotChainManager keeps two in-memory chains that must agree: the 
> global chain (all snapshots in creation order) and the per-path chain 
> (snapshots for a given volume/bucket). GC and reclaim navigate them via 
> previous/next-snapshot lookups.
> 2. A snapshot delete calls deleteSnapshot, which ran deleteSnapshotGlobal(id) 
> && deleteSnapshotPath(path, id). Because of the short-circuit &&, if the 
> global delete commits (returns true) but the path delete then returns false 
> or throws, the global chain is mutated while the path chain is not. If the 
> snapshot is missing from the global chain, the path delete is never attempted 
> at all.
> 3. Either way the two chains diverge: one chain no longer contains the 
> snapshot while the other still links through it. Subsequent 
> previous/next-snapshot navigation returns inconsistent results, which can 
> misdirect reclaim decisions.
> 4. addSnapshot had the mirror-image gap: it committed the global insertion 
> before attempting the path insertion, so a failing path insertion left the 
> snapshot stranded in the global chain.
> 5. Independently, latestGlobalSnapshotId and oldestGlobalSnapshotId were 
> written only under the instance monitor but read by 
> getLatestGlobalSnapshotId/getOldestGlobalSnapshotId (and the chain iterator) 
> without holding it, so a concurrent reader could observe a stale value.
> h3. Root cause
> The dual-chain updates in addSnapshot and deleteSnapshot were not atomic and 
> had no rollback, and deleteSnapshot's short-circuit && could skip or 
> half-apply an update, so a failure or absence in one chain left the two 
> chains inconsistent. The two global head/tail fields were also published 
> without the memory visibility guarantee their unsynchronized getters rely on.
> h3. Suggested fix
> Make both chain mutations all-or-nothing. deleteSnapshot now checks 
> membership in both chains first and refuses a partial delete (throws) when a 
> snapshot is present in only one of them, and rolls back the global deletion 
> (restoreSnapshotGlobal) if the path deletion fails. addSnapshot rolls back 
> the global insertion if the path insertion fails. latestGlobalSnapshotId and 
> oldestGlobalSnapshotId are made volatile so the unsynchronized getters 
> observe the last committed value. Two invariant unit tests are added: a 
> failing path insertion leaves both chains empty (add rollback), and a delete 
> whose path does not match is refused and leaves the global chain unchanged. 
> See [^HDDS-16465.001.patch].
> Note: the underlying trigger is a concurrency window (a failing or asymmetric 
> chain update, or a reader racing a writer) that is timing dependent and not 
> deterministically reproducible without a thread-scheduling seam that shipping 
> code does not expose. The added tests pin the atomicity and partial-update 
> invariants directly rather than reproducing a specific interleaving; the 
> memory-visibility change is asserted by inspection.
> 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