aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorUlf Hermann <ulf.hermann@qt.io>2026-09-14 09:25:10 +0200
committerUlf Hermann <ulf.hermann@qt.io>2026-09-24 06:01:43 +0000
commit059ee9db91507b8b86cdf4bf033985c9ea41e0d4 (patch)
tree7e85d79a15c1df29956b5089e954887cc0f297d7
parent9365808a03fd495b708a43d289514754a6e939b7 (diff)
QtQml: Don't mix up extendsBitmap after allocation-on-sweep
If some object allocates in its destroy() call, the extends bits it consumes would erroneously still get added to the free run in some cases. Pick-to: 6.12 6.8 Task-number: QTBUG-149512 Change-Id: I222922416a4918201afba58dcf63c32af6e45d41 Reviewed-by: Sami Shalayel <sami.shalayel@qt.io>
-rw-r--r--src/qml/memory/qv4mm.cpp22
-rw-r--r--tests/auto/qml/qv4mm/tst_qv4mm.cpp165
2 files changed, 180 insertions, 7 deletions
diff --git a/src/qml/memory/qv4mm.cpp b/src/qml/memory/qv4mm.cpp
index 84d2c2b79f..5bdaf410f6 100644
--- a/src/qml/memory/qv4mm.cpp
+++ b/src/qml/memory/qv4mm.cpp
@@ -262,9 +262,16 @@ bool Chunk::sweep(ExecutionEngine *engine)
HeapItem *o = realBase();
bool lastSlotFree = false;
for (uint i = 0; i < Chunk::EntriesInBitmap; ++i) {
- quintptr toFree = objectBitmap[i] ^ blackBitmap[i];
+ // Snapshots. The destructors below may allocate, and setAllocatedSlots() then
+ // sets object and extends bits in this very word. We have to remember what we
+ // decided to free, so that we can clear exactly that further down rather than
+ // assigning the words wholesale and dropping those bits again.
+ const quintptr objectsToFree = objectBitmap[i] ^ blackBitmap[i];
+ const quintptr extendsBefore = extendsBitmap[i];
+
+ quintptr toFree = objectsToFree;
Q_ASSERT((toFree & objectBitmap[i]) == toFree); // check all black objects are marked as being used
- quintptr e = extendsBitmap[i];
+ quintptr e = extendsBefore;
SDUMP() << " index=" << i;
SDUMP() << " toFree =" << binary(toFree);
SDUMP() << " black =" << binary(blackBitmap[i]);
@@ -298,12 +305,13 @@ bool Chunk::sweep(ExecutionEngine *engine)
heaptrack_report_free(itemToFree);
#endif
}
- Q_V4_PROFILE_DEALLOC(engine, qPopulationCount((objectBitmap[i] | extendsBitmap[i])
- - (blackBitmap[i] | e)) * Chunk::SlotSize,
+ const quintptr extendsToFree = extendsBefore & ~e;
+ Q_V4_PROFILE_DEALLOC(engine,
+ qPopulationCount(objectsToFree | extendsToFree) * Chunk::SlotSize,
Profiling::RegularItem);
- objectBitmap[i] = blackBitmap[i];
- hasUsedSlots |= (blackBitmap[i] != 0);
- extendsBitmap[i] = e;
+ objectBitmap[i] &= ~objectsToFree;
+ extendsBitmap[i] &= ~extendsToFree;
+ hasUsedSlots |= (objectBitmap[i] != 0);
lastSlotFree = !((objectBitmap[i]|extendsBitmap[i]) >> (sizeof(quintptr)*8 - 1));
SDUMP() << " new extends =" << binary(e);
SDUMP() << " lastSlotFree" << lastSlotFree;
diff --git a/tests/auto/qml/qv4mm/tst_qv4mm.cpp b/tests/auto/qml/qv4mm/tst_qv4mm.cpp
index 4927318cd3..51a419b01f 100644
--- a/tests/auto/qml/qv4mm/tst_qv4mm.cpp
+++ b/tests/auto/qml/qv4mm/tst_qv4mm.cpp
@@ -75,6 +75,7 @@ private slots:
void dontCrashOnScopedStackFrame();
void sweepTriggeringChunkAllocation_data();
void sweepTriggeringChunkAllocation();
+ void allocationDuringSweepKeepsAllItsSlots();
void partitionGrowingContainer();
void findObjectsForCompilationUnit_data();
@@ -1429,6 +1430,170 @@ void tst_qv4mm::sweepTriggeringChunkAllocation()
}
}
+QT_BEGIN_NAMESPACE
+
+namespace QV4 {
+
+static std::vector<PersistentValue> allocatedDuringSweep;
+static int allocatedInSweptWord = 0;
+
+namespace Heap {
+
+struct AllocateDuringSweep : Object {
+ void init() { Object::init(); }
+
+ void destroy()
+ {
+ /*
+ Allocating from destroy() is allowed - see sweepTriggeringChunkAllocation -
+ and stock destroy() implementations do it, for instance by way of the
+ Component.onDestruction handlers that ~QQmlContextData emits.
+
+ The free lists we allocate from are still the ones the previous sweep built,
+ so some of these objects land in the very bitmap word Chunk::sweep() is
+ working on. Remember those; they are the interesting ones.
+ */
+ auto v4 = internalClass->engine;
+ const HeapItem *self = reinterpret_cast<const HeapItem *>(this);
+ Chunk *chunk = self->chunk();
+ const size_t word = size_t(self - chunk->realBase()) / Chunk::Bits;
+
+ for (int i = 0; i < 256; ++i) {
+ Heap::Object *o = v4->newObject();
+ const HeapItem *item = reinterpret_cast<const HeapItem *>(o);
+ if (item->chunk() != chunk
+ || size_t(item - chunk->realBase()) / Chunk::Bits != word) {
+ continue;
+ }
+
+ ++allocatedInSweptWord;
+ allocatedDuringSweep.emplace_back(v4, Value::fromHeapObject(o));
+ }
+
+ Object::destroy();
+ }
+};
+
+} // namespace Heap
+
+struct AllocateDuringSweep : Object {
+ V4_OBJECT2(AllocateDuringSweep, Object)
+ V4_NEEDS_DESTROY
+};
+
+DEFINE_OBJECT_VTABLE(AllocateDuringSweep);
+
+namespace Heap {
+
+// Small enough to be served from a single orphaned slot.
+struct OneSlot : Object {
+ void init() { Object::init(); }
+};
+
+} // namespace Heap
+
+struct OneSlot : Object {
+ V4_OBJECT2(OneSlot, Object)
+ enum { NInlineProperties = 0 };
+};
+
+DEFINE_OBJECT_VTABLE(OneSlot);
+
+} // namespace QV4
+
+QT_END_NAMESPACE
+
+void tst_qv4mm::allocationDuringSweepKeepsAllItsSlots()
+{
+ QJSEngine jsEngine;
+ QV4::ExecutionEngine &engine = *jsEngine.handle();
+ QV4::MemoryManager *mm = engine.memoryManager;
+
+ // Leave holes all over the heap, so that the allocations the sweep triggers below
+ // are served from free list entries rather than from a fresh chunk.
+ std::vector<QV4::PersistentValue> keepAlive;
+ mm->gcBlocked = QV4::MemoryManager::InCriticalSection;
+ for (int i = 0; i < 2048; ++i) {
+ QV4::Heap::Object *o = engine.newObject();
+ if (i % 2)
+ keepAlive.emplace_back(&engine, QV4::Value::fromHeapObject(o));
+ }
+ mm->gcBlocked = QV4::MemoryManager::Unblocked;
+ gc(engine);
+
+ QV4::allocatedDuringSweep.clear();
+ QV4::allocatedInSweptWord = 0;
+
+ mm->allocate<QV4::AllocateDuringSweep>();
+ gc(engine);
+
+ // The scenario has to actually occur, otherwise the test proves nothing.
+ QVERIFY(QV4::allocatedInSweptWord > 0);
+
+ // Rooting the new objects in a PersistentValue works: PersistentValue::set runs a
+ // write barrier, the objects are marked, and Chunk::sweep() keeps the object bits
+ // of everything that is black.
+ int stillAllocated = 0;
+ for (const QV4::PersistentValue &value : std::as_const(QV4::allocatedDuringSweep)) {
+ const QV4::HeapItem *item
+ = reinterpret_cast<const QV4::HeapItem *>(value.asManaged()->heapObject());
+ QV4::Chunk *chunk = item->chunk();
+ if (QV4::Chunk::testBit(chunk->objectBitmap, item - chunk->realBase()))
+ ++stillAllocated;
+ }
+ QCOMPARE(stillAllocated, QV4::allocatedInSweptWord);
+
+ /*
+ Their extends bits have to survive too. Chunk::sweep() clears the object and
+ extends bits of a whole 64 slot word once that word's destructors have run, and
+ it must clear exactly the bits it decided to free - not assign the words from
+ values it captured beforehand, which would drop whatever setAllocatedSlots() set
+ in the meantime. Otherwise sortIntoBins(), which derives the free lists from
+ objectBitmap | extendsBitmap, treats the tail slots of these live objects as
+ free.
+ */
+ int freeSlotsInsideLiveObjects = 0;
+ for (const QV4::PersistentValue &value : std::as_const(QV4::allocatedDuringSweep)) {
+ const QV4::HeapItem *owner
+ = reinterpret_cast<const QV4::HeapItem *>(value.asManaged()->heapObject());
+ const size_t nValues = value.asManaged()->vtable()->nInlineProperties
+ + value.asManaged()->vtable()->inlinePropertyOffset;
+ const size_t nSlots = nValues * sizeof(QV4::Value) / QV4::Chunk::SlotSize;
+
+ for (int bin = 0; bin < QV4::BlockAllocator::NumBins; ++bin) {
+ for (const QV4::HeapItem *item = mm->blockAllocator.freeBins[bin]; item;
+ item = item->freeData.next) {
+ if (item > owner && item < owner + nSlots)
+ ++freeSlotsInsideLiveObjects;
+ }
+ }
+ }
+
+ QCOMPARE(freeSlotsInsideLiveObjects, 0);
+
+ // Which is not merely a bookkeeping problem: a request small enough to be served
+ // from one of those slots gets handed a piece of a live object.
+ int allocatedInsideLiveObject = 0;
+ mm->gcBlocked = QV4::MemoryManager::InCriticalSection;
+ for (int i = 0; i < 512; ++i) {
+ const QV4::HeapItem *item = reinterpret_cast<const QV4::HeapItem *>(
+ mm->allocate<QV4::OneSlot>());
+ for (const QV4::PersistentValue &value : std::as_const(QV4::allocatedDuringSweep)) {
+ const QV4::HeapItem *owner
+ = reinterpret_cast<const QV4::HeapItem *>(value.asManaged()->heapObject());
+ const size_t nValues = value.asManaged()->vtable()->nInlineProperties
+ + value.asManaged()->vtable()->inlinePropertyOffset;
+ const size_t nSlots = nValues * sizeof(QV4::Value) / QV4::Chunk::SlotSize;
+ if (item > owner && item < owner + nSlots)
+ ++allocatedInsideLiveObject;
+ }
+ }
+ mm->gcBlocked = QV4::MemoryManager::Unblocked;
+ QCOMPARE(allocatedInsideLiveObject, 0);
+
+ QV4::allocatedDuringSweep.clear();
+}
+
void tst_qv4mm::partitionGrowingContainer()
{
std::vector<int> prePopulated;