diff options
| author | Ulf Hermann <ulf.hermann@qt.io> | 2026-09-14 09:25:10 +0200 |
|---|---|---|
| committer | Ulf Hermann <ulf.hermann@qt.io> | 2026-09-24 06:01:43 +0000 |
| commit | 059ee9db91507b8b86cdf4bf033985c9ea41e0d4 (patch) | |
| tree | 7e85d79a15c1df29956b5089e954887cc0f297d7 | |
| parent | 9365808a03fd495b708a43d289514754a6e939b7 (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.cpp | 22 | ||||
| -rw-r--r-- | tests/auto/qml/qv4mm/tst_qv4mm.cpp | 165 |
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; |
