Conversation
…semble Removing a source case makes a delta ensemble recreate and delete its derived cases. Those cases are part of the list being removed, leaving dangling pointers that crash in PdmObjectHandle::prepareForDelete(). Use guarded pointers and return only the surviving cases.
Delta ensembles were located by scanning the summary case main collection for objects referring to a given ensemble, and the dependent ensembles were visited by unguarded recursion. A dependency cycle, which is constructible through the UI, made that recursion run forever, and an ensemble used as both source 1 and source 2 was reported twice. Add dependentDeltaEnsembles(), deltaEnsemblesInUpdateOrder() and wouldCreateDependencyCycle() to RimSummaryEnsembleTools. The traversal uses the PDM back references, deduplicates, is iterative with visited and on-path sets, and returns the delta ensembles in topological order so a delta ensemble is always visited before the delta ensembles using it as a source. Back edges are logged instead of traversed. Reimplement updateDependentDeltaEnsembles on top of the new traversal and replace RimDeltaSummaryEnsemble::findReferringEnsembles() with dependentDeltaEnsembles() at its four call sites. Back references also find a delta ensemble that is detached from the project tree, which the previous ancestor scan did not.
Derived cases were pooled through an m_inUse flag on RimDeltaSummaryCase. Every regeneration marked all cases not in use, which also severed their source references and cleared their caches, then handed them back out one by one and deleted whatever was left over. The flag conflated pool bookkeeping with owning the source references, allSummaryCases() hid the not-in-use cases from the rest of the project, and cases taken from the pool were pushed straight into m_cases without connecting nameChanged, so a renamed source case never propagated to the derived case. Replace the pooling with desiredSourceCasePairs(), a pure computation of the source case pairs the ensemble should have, and rebuildDerivedCases(), which diffs that against the existing derived cases keyed on the source case pointer pair. Matching cases are reused, missing ones are created, and surplus ones are detached and returned to the caller instead of being deleted in place. Keying on the pointer pair makes the rebuild idempotent, also right after project load where the derived cases arrive from XML with their sources already resolved. Add the protected RimSummaryEnsemble::addCaseWithoutDependencyUpdate(), used both by addCase() and by the rebuild, so a derived case gets nameChanged connected without triggering the dependent-ensemble notification that the rebuild is already performing itself. Remove setAllCasesNotInUse(), firstCaseNotInUse(), deleteCasesNoInUse(), RimDeltaSummaryCase::setInUse()/isInUse() and the m_inUse field, and add clearSourceCases() for the one thing setInUse(false) was actually needed for. Old project files keep loading, unknown XML keywords are skipped. The activeOnly parameter of allDerivedCases() is gone and RimDeltaSummaryEnsemble no longer overrides allSummaryCases().
Closing all summary cases while a delta ensemble is present crashed with a use-after-free. RicCloseSummaryCaseFeature::deleteSummaryCases holds a case list across removeCases and deletes it afterwards, while removeCases made the delta ensemble rebuild and destroy derived cases that are themselves part of that list. The previous fix rewrote the caller list with the surviving cases, which stopped the crash but left the hazard in place for any other caller holding a case list across a removal. Add RimSummaryCaseUpdateBatch, a plain scope object that is ambient for the duration of its scope. Removal now only detaches, and hands the detached cases to the batch. The outermost scope flushes, regenerating the dirty delta ensembles in dependency order first so a chained delta ensemble sees the final state of its source, then destroying the orphans with caf::PdmObjectHandleTools::deleteObjects. A nested batch contributes to the outermost one and never flushes. Orphans are held as guarded pointers, so a case the caller destroyed itself is skipped instead of being destroyed twice. Both contribution points fall back to immediate execution when no batch is active, so call sites that do not open one keep behaving as before. Open a batch in RimSummaryCaseMainCollection::removeCases and in RicCloseSummaryCaseFeature::deleteSummaryCases, which is the outermost of the two and therefore keeps the detached cases alive across its own deleteObjects call. removeCases no longer rewrites the caller list, so it now takes it by const reference.
RimDeltaSummaryEnsemble-Test.cpp and RimSummaryCaseMainCollection-Test.cpp each defined an identical createMockCase() in an anonymous namespace. A unity build concatenates the two translation units, which merges the two anonymous namespaces into one and makes the second definition a redefinition, breaking the build with C2084. Move the factory to RimMockSummaryCase.h, the header both tests already include for the mock case itself, and drop both local copies.
The project is a global object shared by all tests, and the MSW export tests loaded a project without closing it. The two summary cases of that project stayed in the summary case main collection, and made RimSummaryCaseMainCollection.RemoveCases_NoDanglingInCallerVector fail on Windows only. Google Test runs the test suites in link order, and the MSW suite runs before the summary suite on Windows and after it on Linux. Add a test event listener reporting a failure for the test leaving cases, ensembles, well paths or views behind in the project. The project is closed as well, so the tests running after the offending one are unaffected.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stages 1–3 of the staged redesign in #1031. Together they retire the use-after-free behind OPM#14423 structurally, rather than working around it at one call site. Stages 4–7, which cover the silent-wrong-state defects, are not included.
Part of #1031.
Base
The first commit is a cherry-pick of
e16238f331, the band-aid from the open OPM#14424, so the regression test it introduced is present for stage 3 to build on. Stage 3 then removes the band-aid itself. If OPM#14424 merges upstream first, that commit drops out on rebase and nothing else changes.Stage 1 — cycle-safe dependency traversal
Delta ensembles were found by scanning the summary case main collection, and their dependents visited by unguarded recursion. A dependency cycle, constructible through the UI, made that recursion run forever, and an ensemble used as both source 1 and source 2 was reported twice.
RimSummaryEnsembleToolsgainsdependentDeltaEnsembles(),deltaEnsemblesInUpdateOrder()andwouldCreateDependencyCycle(). The traversal follows the PDM back references, deduplicates, is iterative with visited and on-path sets, and returns delta ensembles in topological order. Back edges are logged rather than traversed.RimDeltaSummaryEnsemble::findReferringEnsembles()is deleted and its four call sites converted.Stage 2 — declarative rebuild,
setInUsepooling deletedDerived cases were pooled through an
m_inUseflag that conflated pool bookkeeping with owning the source references and the value cache.allSummaryCases()hid the not-in-use cases from the rest of the project, and cases handed out of the pool never gotnameChangedconnected, so a renamed source case never propagated.Replaced by
desiredSourceCasePairs(), a pure computation of the pairs the ensemble should have, andrebuildDerivedCases(), which diffs that against the existing derived cases keyed on the(case1, case2)pointer pair. Matching cases are reused, missing ones created, surplus ones detached and returned to the caller instead of deleted in place. Keying on the pointer pair makes the rebuild idempotent, including right after project load.setInUse,isInUse,m_inUse,setAllCasesNotInUse,firstCaseNotInUseanddeleteCasesNoInUseare gone; old project files still load, since unknown XML keywords are skipped.Stage 3 —
RimSummaryCaseUpdateBatchRicCloseSummaryCaseFeature::deleteSummaryCasesholds a case list acrossremoveCasesand deletes it afterwards, whileremoveCasesmade a delta ensemble destroy derived cases that are themselves in that list.RimSummaryCaseUpdateBatchis a plain scope object, ambient for its scope. Removal only detaches and hands the cases to the batch; the outermost scope flushes, regenerating dirty delta ensembles in dependency order first, then destroying the orphans. A nested batch contributes to the outermost one and never flushes. Orphans are held as guarded pointers, so a case the caller already destroyed is skipped rather than destroyed twice. Both contribution points fall back to immediate execution when no batch is active.Deviation from the plan
The plan puts the outermost batch in
removeCases. That does not work:deleteSummaryCasescallscaf::PdmObjectHandleTools::deleteObjects( cases )afterremoveCaseshas returned, so a batch ending there frees the orphans while that list still refers to them and the crash survives. A batch is therefore opened in both places, and nesting makes the feature's the outermost.removeCasesno longer rewrites the caller's list and now takes it by const reference.The regression test could not be kept verbatim for the same reason — its
aliveCount == cases.size()assertion tests the write-back, which is the band-aid. It is renamed toRemoveCases_NoDanglingInCallerVector, as the plan's own test table anticipated, and asserts the invariant the batch buys instead: inside the batch scope every entry the caller handed over is still alive, and after the scope exits each has been destroyed exactly once. Removing just the batch line from that test on this branch reproduces the original signature exactly,cases.size()6 againstaliveCount()4 followed bySEH exception with code 0xc0000005.Follow-up spotted along the way
A dependency cycle stack-overflows in auto-name generation, independently of the traversal fixed here:
RimSummaryEnsemble::updateNameends incaseNameChanged.send(), which reachesRimSummaryCaseMainCollection::onCaseNameChangedand back intoupdateSummaryEnsembleNames(), terminating only because names converge — which they never do when A's name derives from B's and B's from A's.DependencyOrder_CycleTerminatescloses its cycle by writing thePdmPtrFielddirectly for that reason. Stage 7 already plans cycle prevention in the setters and should cover it.