#14505 caf: Add cafTestApplication repro for reentrant property editor rebuild

Adds a ReentrantEditorRebuild demo object that rebuilds all property editors from inside fieldChangedByUi(), while Qt is still executing QLineEdit::focusOutEvent. Type a value into Realization Filter and press Tab to trigger. The change handler drops both the line edit field and its parent group from the ui ordering, so PdmUiFormLayoutObjectEditor::configureAndUpdateUi() tears down widgets that are still in use further up the stack. Uncheck Rebuild Layout On Change to run the same edit without the reentrant rebuild.
This commit is contained in:
Magne Sjaastad committed 2026-08-11 08:36:37 +02:00
1 parent 450d567ab1
commit 702e7e3173
4 files changed
+133

No files matched your search

@@ -64,6 +64,8 @@ set(PROJECT_FILES
PdmObjects/OptionalFields.cpp
PdmObjects/ValidationTest.h
PdmObjects/ValidationTest.cpp
PdmObjects/ReentrantEditorRebuild.h
PdmObjects/ReentrantEditorRebuild.cpp
)
qt_add_executable(
@@ -13,6 +13,7 @@
#include "PdmObjects/LineEditAndPushButtons.h"
#include "PdmObjects/ManyGroups.h"
#include "PdmObjects/OptionalFields.h"
#include "PdmObjects/ReentrantEditorRebuild.h"
#include "PdmObjects/SingleEditorPdmObject.h"
#include "PdmObjects/SmallDemoPdmObject.h"
#include "PdmObjects/SmallDemoPdmObjectA.h"
@@ -196,6 +197,7 @@ void MainWindow::buildTestModel()
m_testRoot->objects.push_back( new LineEditAndPushButtons );
m_testRoot->objects.push_back( new LabelsAndHyperlinks );
m_testRoot->objects.push_back( new OptionalFields );
m_testRoot->objects.push_back( new ReentrantEditorRebuild );
auto tamComboBox = new TamComboBox;
m_testRoot->objects.push_back( tamComboBox );
@@ -0,0 +1,89 @@
#include "ReentrantEditorRebuild.h"
#include "cafPdmUiLineEditor.h"
#include "cafPdmUiOrdering.h"
CAF_PDM_SOURCE_INIT( ReentrantEditorRebuild, "ReentrantEditorRebuild" );
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
ReentrantEditorRebuild::ReentrantEditorRebuild()
: m_hideFilterGroup( false )
{
CAF_PDM_InitObject( "Reentrant Editor Rebuild", "", "", "" );
CAF_PDM_InitField( &m_realizationFilter,
"RealizationFilter",
QString(),
"Realization Filter",
"",
"Type a value here and press Tab",
"" );
CAF_PDM_InitField( &m_statusField, "Status", QString( "Type into Realization Filter, then press Tab" ), "Status", "", "", "" );
m_statusField.uiCapability()->setUiReadOnly( true );
CAF_PDM_InitField( &m_rebuildLayoutOnChange,
"RebuildLayoutOnChange",
true,
"Rebuild Layout On Change",
"",
"Rebuild all property editors from inside fieldChangedByUi()",
"" );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void ReentrantEditorRebuild::fieldChangedByUi( const caf::PdmFieldHandle* changedField,
const QVariant& oldValue,
const QVariant& newValue )
{
if ( changedField == &m_realizationFilter )
{
m_statusField = "Committed value: " + m_realizationFilter();
if ( m_rebuildLayoutOnChange )
{
// Drop the group that parents the line edit, and rebuild every property editor while
// still inside QLineEdit::focusOutEvent. This mirrors
// RimSumoDataSource::fieldChangedByUi() -> RimSummaryEnsembleSumo::onRealizationSelectionChanged().
m_hideFilterGroup = true;
updateAllRequiredEditors();
}
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void ReentrantEditorRebuild::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
{
uiOrdering.add( &m_statusField );
uiOrdering.add( &m_rebuildLayoutOnChange );
if ( !m_hideFilterGroup )
{
// The line edit lives inside this group. When both the field and the group disappear from
// the ui ordering, PdmUiFormLayoutObjectEditor::configureAndUpdateUi() first deletes the
// field editor, whose destructor only calls deleteLater() on the QLineEdit, and then
// destroys the parent QMinimizePanel with a raw delete. ~QWidget destroys its children
// immediately, so the QLineEdit is gone before the pending deleteLater() can run.
auto group = uiOrdering.addNewGroup( "Filter" );
group->add( &m_realizationFilter );
}
uiOrdering.addNewButton( "Restore Filter Group", [this]() { restoreFilterGroup(); } );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void ReentrantEditorRebuild::restoreFilterGroup()
{
m_hideFilterGroup = false;
updateAllRequiredEditors();
}
@@ -0,0 +1,40 @@
#pragma once
#include "cafPdmField.h"
#include "cafPdmObject.h"
//==================================================================================================
/// Reproduction of https://github.com/OPM/ResInsight/issues/14505
///
/// Rebuilding the property editors from inside fieldChangedByUi() destroys widgets that Qt is
/// still executing an event handler on, giving a use-after-free in QLineEdit::focusOutEvent.
///
/// Steps to reproduce:
/// 1. Select this object in the tree view so the property editor is shown.
/// 2. Type a value into "Realization Filter".
/// 3. Press Tab. Do not press Enter, the focus-out path is the dangerous one. Enter is handled
/// by PdmUiLineEditor::eventFilter, which calls slotEditingFinished() outside focusOutEvent.
///
/// Uncheck "Rebuild Layout On Change" to get the same edit without the reentrant rebuild.
//==================================================================================================
class ReentrantEditorRebuild : public caf::PdmObject
{
CAF_PDM_HEADER_INIT;
public:
ReentrantEditorRebuild();
void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override;
private:
void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override;
void restoreFilterGroup();
private:
caf::PdmField<QString> m_realizationFilter;
caf::PdmField<QString> m_statusField;
caf::PdmField<bool> m_rebuildLayoutOnChange;
bool m_hideFilterGroup;
};