[K/N] Crash with OOM on large array allocations ^KT-54659

Merge-request: KT-MR-7507
Merged-by: Alexander Shabalin <Alexander.Shabalin@jetbrains.com>
This commit is contained in:
Alexander Shabalin
2022-11-01 11:00:23 +00:00
committed by Space Team
parent cc4d22b4c9
commit 5ef9a5a240
12 changed files with 116 additions and 61 deletions
@@ -3441,6 +3441,24 @@ standaloneTest("stress_gc_allocations") {
flags = ['-tr', '-opt-in=kotlin.native.internal.InternalForKotlinNative']
}
standaloneTest("array_out_of_memory") {
source = "runtime/memory/array_out_of_memory.kt"
flags = ['-tr']
switch(project.target.architecture) {
case Architecture.X64:
case Architecture.ARM64:
break;
case Architecture.X86:
case Architecture.ARM32:
case Architecture.MIPS32:
case Architecture.MIPSEL32:
case Architecture.WASM32:
expectedExitStatusChecker = { it != 0 }
outputChecker = { s -> s.contains("Out of memory trying to allocate") }
break;
}
}
standaloneTest("mpp1") {
source = "codegen/mpp/mpp1.kt"
flags = ['-tr', '-Xmulti-platform']
@@ -0,0 +1,26 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
import kotlin.test.*
fun testArrayAllocation(size: Int) {
val arr = IntArray(size)
// Force a write into the memory.
// TODO: How to make sure the optimizer never deletes this write?
arr[size - 1] = 42
assertEquals(42, arr[size - 1])
}
@Test
fun sanity() {
// Should always succeed everywhere
testArrayAllocation(1 shl 10)
}
@Test
fun test() {
// Will fail on 32 bits.
testArrayAllocation(1 shl 30)
}
@@ -54,7 +54,7 @@ ALWAYS_INLINE ObjHeader* gc::GC::ThreadData::CreateObject(const TypeInfo* typeIn
return impl_->objectFactoryThreadQueue().CreateObject(typeInfo);
}
ALWAYS_INLINE ArrayHeader* gc::GC::ThreadData::CreateArray(const TypeInfo* typeInfo, uint32_t elements) {
ALWAYS_INLINE ArrayHeader* gc::GC::ThreadData::CreateArray(const TypeInfo* typeInfo, uint32_t elements) noexcept {
return impl_->objectFactoryThreadQueue().CreateArray(typeInfo, elements);
}
@@ -42,7 +42,7 @@ public:
void ClearForTests() noexcept;
ObjHeader* CreateObject(const TypeInfo* typeInfo) noexcept;
ArrayHeader* CreateArray(const TypeInfo* typeInfo, uint32_t elements);
ArrayHeader* CreateArray(const TypeInfo* typeInfo, uint32_t elements) noexcept;
void OnStoppedForGC() noexcept;
void OnSuspendForGC() noexcept;
@@ -44,7 +44,7 @@ ALWAYS_INLINE ObjHeader* gc::GC::ThreadData::CreateObject(const TypeInfo* typeIn
return impl_->objectFactoryThreadQueue().CreateObject(typeInfo);
}
ALWAYS_INLINE ArrayHeader* gc::GC::ThreadData::CreateArray(const TypeInfo* typeInfo, uint32_t elements) {
ALWAYS_INLINE ArrayHeader* gc::GC::ThreadData::CreateArray(const TypeInfo* typeInfo, uint32_t elements) noexcept {
return impl_->objectFactoryThreadQueue().CreateArray(typeInfo, elements);
}
@@ -57,7 +57,7 @@ ALWAYS_INLINE ObjHeader* gc::GC::ThreadData::CreateObject(const TypeInfo* typeIn
return impl_->objectFactoryThreadQueue().CreateObject(typeInfo);
}
ALWAYS_INLINE ArrayHeader* gc::GC::ThreadData::CreateArray(const TypeInfo* typeInfo, uint32_t elements) {
ALWAYS_INLINE ArrayHeader* gc::GC::ThreadData::CreateArray(const TypeInfo* typeInfo, uint32_t elements) noexcept {
return impl_->objectFactoryThreadQueue().CreateArray(typeInfo, elements);
}
@@ -17,6 +17,7 @@
#include <string.h>
#include <stdio.h>
#include <cinttypes>
#include <cstddef> // for offsetof
#include <mutex>
@@ -32,6 +33,7 @@
#endif
#include "KAssert.h"
#include "Alignment.hpp"
#include "Atomic.h"
#include "Cleaner.h"
#include "CompilerConstants.hpp"
@@ -92,7 +94,7 @@ ALWAYS_INLINE bool IsStrictMemoryModel() noexcept {
inline constexpr ObjectPoolAllocator<char> objectAllocator;
typedef uint32_t container_size_t;
using container_size_t = size_t;
// Granularity of arena container chunks.
constexpr container_size_t kContainerAlignment = 1024;
@@ -913,7 +915,7 @@ class ArenaContainer {
// Places an array of certain type in this container. Note that array_type_info
// is type info for an array, not for an individual element. Also note that exactly
// same operation could be used to place strings.
ArrayHeader* PlaceArray(const TypeInfo* array_type_info, container_size_t count);
ArrayHeader* PlaceArray(const TypeInfo* array_type_info, uint32_t count);
ObjHeader** getSlot();
@@ -962,23 +964,23 @@ inline ContainerHeader* clearRemoved(ContainerHeader* container) {
reinterpret_cast<uintptr_t>(container) & ~static_cast<uintptr_t>(1));
}
inline container_size_t alignUp(container_size_t size, int alignment) {
return (size + alignment - 1) & ~(alignment - 1);
}
inline ContainerHeader* realShareableContainer(ContainerHeader* container) {
RuntimeAssert(container->shareable(), "Only makes sense on shareable objects");
return containerFor(reinterpret_cast<ObjHeader*>(container + 1));
}
inline uint32_t arrayObjectSize(const TypeInfo* typeInfo, uint32_t count) {
// Note: array body is aligned, but for size computation it is enough to align the sum.
inline uint64_t arrayObjectSize(const TypeInfo* typeInfo, uint32_t count) {
static_assert(kObjectAlignment % alignof(KLong) == 0, "");
static_assert(kObjectAlignment % alignof(KDouble) == 0, "");
return alignUp(sizeof(ArrayHeader) - typeInfo->instanceSize_ * count, kObjectAlignment);
// -(int32_t min) * uint32_t max cannot overflow uint64_t. And are capped
// at about half of uint64_t max.
uint64_t membersSize = static_cast<uint64_t>(-typeInfo->instanceSize_) * count;
// Note: array body is aligned, but for size computation it is enough to align the sum.
return AlignUp<uint64_t>(sizeof(ArrayHeader) + membersSize, kObjectAlignment);
}
inline uint32_t arrayObjectSize(const ArrayHeader* obj) {
inline container_size_t arrayObjectSize(const ArrayHeader* obj) {
// Only used for already allocated arrays. Cannov overflow size_t.
return arrayObjectSize(obj->type_info(), obj->count_);
}
@@ -990,7 +992,7 @@ inline container_size_t objectSize(const ObjHeader* obj) {
arrayObjectSize(obj->array())
:
type_info->instanceSize_);
return alignUp(size, kObjectAlignment);
return kotlin::AlignUp(size, kObjectAlignment);
}
template <typename func>
@@ -1079,7 +1081,7 @@ ContainerHeader* allocContainer(MemoryState* state, size_t size) {
if (state != nullptr)
state->allocSinceLastGc += size;
#endif
result = new (allocateInObjectPool(alignUp(size, kObjectAlignment))) ContainerHeader();
result = new (allocateInObjectPool(kotlin::AlignUp(size, kObjectAlignment))) ContainerHeader();
atomicAdd(&allocCount, 1);
}
if (state != nullptr) {
@@ -1780,11 +1782,11 @@ inline ArenaContainer* initedArena(ObjHeader** auxSlot) {
return arena;
}
inline size_t containerSize(const ContainerHeader* container) {
size_t result = 0;
inline container_size_t containerSize(const ContainerHeader* container) {
container_size_t result = 0;
const ObjHeader* obj = reinterpret_cast<const ObjHeader*>(container + 1);
for (uint32_t object = 0; object < container->objectCount(); object++) {
size_t size = objectSize(obj);
container_size_t size = objectSize(obj);
result += size;
obj = reinterpret_cast<ObjHeader*>(reinterpret_cast<uintptr_t>(obj) + size);
}
@@ -3188,7 +3190,7 @@ void ObjHeader::destroyMetaObject(ObjHeader* object) {
void ObjectContainer::Init(MemoryState* state, const TypeInfo* typeInfo) {
RuntimeAssert(typeInfo->instanceSize_ >= 0, "Must be an object");
uint32_t allocSize = sizeof(ContainerHeader) + typeInfo->instanceSize_;
container_size_t allocSize = sizeof(ContainerHeader) + typeInfo->instanceSize_;
header_ = allocContainer(state, allocSize);
RuntimeCheck(header_ != nullptr, "Cannot alloc memory");
// One object in this container, no need to set.
@@ -3201,8 +3203,12 @@ void ObjectContainer::Init(MemoryState* state, const TypeInfo* typeInfo) {
void ArrayContainer::Init(MemoryState* state, const TypeInfo* typeInfo, uint32_t elements) {
RuntimeAssert(typeInfo->instanceSize_ < 0, "Must be an array");
uint32_t allocSize =
sizeof(ContainerHeader) + arrayObjectSize(typeInfo, elements);
uint64_t dataSize = arrayObjectSize(typeInfo, elements);
uint64_t allocSize = sizeof(ContainerHeader) + dataSize;
if (allocSize > std::numeric_limits<size_t>::max()) {
konan::consoleErrorf("Out of memory trying to allocate %" PRIu64 " bytes. Aborting.\n", allocSize);
konan::abort();
}
header_ = allocContainer(state, allocSize);
RuntimeCheck(header_ != nullptr, "Cannot alloc memory");
// One object in this container, no need to set.
@@ -3211,7 +3217,7 @@ void ArrayContainer::Init(MemoryState* state, const TypeInfo* typeInfo, uint32_t
// header->refCount_ is zero initialized by allocContainer().
GetPlace()->count_ = elements;
SetHeader(GetPlace()->obj(), typeInfo);
OBJECT_ALLOC_EVENT(memoryState, arrayObjectSize(typeInfo, elements), GetPlace()->obj())
OBJECT_ALLOC_EVENT(memoryState, dataSize, GetPlace()->obj())
}
// TODO: store arena containers in some reuseable data structure, similar to
@@ -3239,7 +3245,7 @@ void ArenaContainer::Deinit() {
bool ArenaContainer::allocContainer(container_size_t minSize) {
auto size = minSize + sizeof(ContainerHeader) + sizeof(ContainerChunk);
size = alignUp(size, kContainerAlignment);
size = kotlin::AlignUp(size, kContainerAlignment);
// TODO: keep simple cache of container chunks.
ContainerChunk* result = new (allocateInObjectPool(size)) ContainerChunk();
RuntimeCheck(result != nullptr, "Cannot alloc memory");
@@ -3254,7 +3260,7 @@ bool ArenaContainer::allocContainer(container_size_t minSize) {
}
void* ArenaContainer::place(container_size_t size) {
size = alignUp(size, kObjectAlignment);
size = kotlin::AlignUp(size, kObjectAlignment);
// Fast path.
if (current_ + size < end_) {
void* result = current_;
@@ -3282,7 +3288,7 @@ ObjHeader** ArenaContainer::getSlot() {
ObjHeader* ArenaContainer::PlaceObject(const TypeInfo* type_info) {
RuntimeAssert(type_info->instanceSize_ >= 0, "must be an object");
uint32_t size = type_info->instanceSize_;
container_size_t size = type_info->instanceSize_;
ObjHeader* result = reinterpret_cast<ObjHeader*>(place(size));
if (!result) {
return nullptr;
@@ -3295,12 +3301,16 @@ ObjHeader* ArenaContainer::PlaceObject(const TypeInfo* type_info) {
ArrayHeader* ArenaContainer::PlaceArray(const TypeInfo* type_info, uint32_t count) {
RuntimeAssert(type_info->instanceSize_ < 0, "must be an array");
container_size_t size = arrayObjectSize(type_info, count);
uint64_t size = arrayObjectSize(type_info, count);
if (size > std::numeric_limits<size_t>::max()) {
konan::consoleErrorf("Out of memory trying to allocate %" PRIu64 " bytes. Aborting.\n", size);
konan::abort();
}
ArrayHeader* result = reinterpret_cast<ArrayHeader*>(place(size));
if (!result) {
return nullptr;
}
OBJECT_ALLOC_EVENT(memoryState, arrayObjectSize(type_info, count), result->obj())
OBJECT_ALLOC_EVENT(memoryState, size, result->obj())
currentChunk_->asHeader()->incObjectCount();
setHeader(result->obj(), type_info);
result->count_ = count;
@@ -13,7 +13,8 @@ namespace kotlin {
constexpr size_t kObjectAlignment = 8;
constexpr inline size_t AlignUp(size_t size, size_t alignment) {
template <typename T>
constexpr T AlignUp(T size, T alignment) {
return (size + alignment - 1) & ~(alignment - 1);
}
@@ -3,6 +3,8 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
#pragma once
#if __has_include(<optional>)
#include <optional>
#elif __has_include(<experimental/optional>)
@@ -15,4 +17,4 @@ inline constexpr auto nullopt = std::experimental::nullopt;
} // namespace std
#else
#error "No <optional>"
#endif
#endif
@@ -7,12 +7,12 @@
#define RUNTIME_MM_OBJECT_FACTORY_H
#include <algorithm>
#include <cinttypes>
#include <memory>
#include <mutex>
#include <type_traits>
#include "Alignment.hpp"
#include "Exceptions.h"
#include "FinalizerHooks.hpp"
#include "Memory.h"
#include "Mutex.hpp"
@@ -54,9 +54,9 @@ public:
public:
~Node() = default;
constexpr static size_t GetSizeForDataSize(size_t dataSize) noexcept {
size_t dataSizeAligned = AlignUp(dataSize, DataAlignment);
size_t totalSize = AlignUp(sizeof(Node) + dataSizeAligned, DataAlignment);
constexpr static uint64_t GetSizeForDataSize(uint64_t dataSize) noexcept {
uint64_t dataSizeAligned = AlignUp<uint64_t>(dataSize, DataAlignment);
uint64_t totalSize = AlignUp<uint64_t>(sizeof(Node) + dataSizeAligned, DataAlignment);
return totalSize;
}
@@ -86,14 +86,19 @@ public:
Node() noexcept = default;
static unique_ptr<Node> Create(Allocator& allocator, size_t dataSize) noexcept {
static unique_ptr<Node> Create(Allocator& allocator, uint64_t dataSize) noexcept {
auto totalSize = GetSizeForDataSize(dataSize);
RuntimeAssert(
DataOffset() + dataSize <= totalSize, "totalSize %zu is not enough to fit data %zu at offset %zu", totalSize, dataSize,
DataOffset());
void* ptr = allocator.Alloc(totalSize);
void* ptr = nullptr;
if (totalSize <= std::numeric_limits<size_t>::max()) {
RuntimeAssert(
DataOffset() + dataSize <= totalSize, "totalSize %" PRIu64 " is not enough to fit data %" PRIu64 " at offset %zu", totalSize, dataSize,
DataOffset());
ptr = allocator.Alloc(totalSize);
}
if (!ptr) {
konan::consoleErrorf("Out of memory trying to allocate %zu bytes. Aborting.\n", totalSize);
// TODO: This should throw OutOfMemoryError in the future if we add hard memory limits instead
// of limiting at virtual address space boundary.
konan::consoleErrorf("Out of memory trying to allocate %" PRIu64 " bytes. Aborting.\n", totalSize);
konan::abort();
}
RuntimeAssert(IsAligned(ptr, DataAlignment), "Allocator returned unaligned to %zu pointer %p", DataAlignment, ptr);
@@ -134,7 +139,7 @@ public:
size_t size() const noexcept { return size_; }
Node& Insert(size_t dataSize) noexcept {
Node& Insert(uint64_t dataSize) noexcept {
AssertCorrect();
auto node = Node::Create(allocator_, dataSize);
auto* nodePtr = node.get();
@@ -513,7 +518,8 @@ public:
static size_t ObjectAllocatedSize(const TypeInfo* typeInfo) noexcept {
RuntimeAssert(!typeInfo->IsArray(), "Must not be an array");
size_t allocSize = ObjectAllocatedDataSize(typeInfo);
// Only used for already allocated objects. Cannot overflow size_t
auto allocSize = ObjectAllocatedDataSize(typeInfo);
return Storage::Node::GetSizeForDataSize(allocSize);
}
@@ -530,21 +536,14 @@ public:
static size_t ArrayAllocatedSize(const TypeInfo* typeInfo, uint32_t count) noexcept {
RuntimeAssert(typeInfo->IsArray(), "Must be an array");
size_t allocSize = ArrayAllocatedDataSize(typeInfo, count);
// Only used for already allocated arrays. Cannot overflow size_t
auto allocSize = ArrayAllocatedDataSize(typeInfo, count);
return Storage::Node::GetSizeForDataSize(allocSize);
}
ArrayHeader* CreateArray(const TypeInfo* typeInfo, uint32_t count) {
ArrayHeader* CreateArray(const TypeInfo* typeInfo, uint32_t count) noexcept {
RuntimeAssert(typeInfo->IsArray(), "Must be an array");
size_t allocSize = ArrayAllocatedDataSize(typeInfo, count);
// On 32-bit systems, overflow can happen in several places along
// the size calculation. If overflow did not happen in the
// multiplication checked in the previous call, but any of the
// subsequent additions overflowed, then the overflowed value will
// be small compared to the number of entries in the array.
if (Storage::Node::GetSizeForDataSize(allocSize) < count) {
ThrowOutOfMemoryError();
}
auto allocSize = ArrayAllocatedDataSize(typeInfo, count);
auto& node = producer_.Insert(allocSize);
auto* heapArray = new (node.Data()) HeapArrayHeader();
auto* array = &heapArray->array;
@@ -567,13 +566,12 @@ public:
return AlignUp(sizeof(HeapObjHeader) + membersSize, kObjectAlignment);
}
static size_t ArrayAllocatedDataSize(const TypeInfo* typeInfo, uint32_t count) noexcept {
size_t membersSize;
if (__builtin_mul_overflow(static_cast<size_t>(-typeInfo->instanceSize_), count, &membersSize)) {
ThrowOutOfMemoryError();
}
static uint64_t ArrayAllocatedDataSize(const TypeInfo* typeInfo, uint32_t count) noexcept {
// -(int32_t min) * uint32_t max cannot overflow uint64_t. And are capped
// at about half of uint64_t max.
uint64_t membersSize = static_cast<uint64_t>(-typeInfo->instanceSize_) * count;
// Note: array body is aligned, but for size computation it is enough to align the sum.
return AlignUp(sizeof(HeapArrayHeader) + membersSize, kObjectAlignment);
return AlignUp<uint64_t>(sizeof(HeapArrayHeader) + membersSize, kObjectAlignment);
}
typename Storage::Producer producer_;
@@ -62,7 +62,7 @@ OBJ_GETTER(mm::AllocateObject, ThreadData* threadData, const TypeInfo* typeInfo)
RETURN_OBJ(object);
}
OBJ_GETTER(mm::AllocateArray, ThreadData* threadData, const TypeInfo* typeInfo, uint32_t elements) {
OBJ_GETTER(mm::AllocateArray, ThreadData* threadData, const TypeInfo* typeInfo, uint32_t elements) noexcept {
AssertThreadState(threadData, ThreadState::kRunnable);
// TODO: Make this work with GCs that can stop thread at any point.
auto* array = threadData->gc().CreateArray(typeInfo, static_cast<uint32_t>(elements));
@@ -27,7 +27,7 @@ void SetHeapRefAtomic(ObjHeader** location, ObjHeader* value) noexcept;
OBJ_GETTER(ReadHeapRefAtomic, ObjHeader** location) noexcept;
OBJ_GETTER(CompareAndSwapHeapRef, ObjHeader** location, ObjHeader* expected, ObjHeader* value) noexcept;
OBJ_GETTER(AllocateObject, ThreadData* threadData, const TypeInfo* typeInfo) noexcept;
OBJ_GETTER(AllocateArray, ThreadData* threadData, const TypeInfo* typeInfo, uint32_t elements);
OBJ_GETTER(AllocateArray, ThreadData* threadData, const TypeInfo* typeInfo, uint32_t elements) noexcept;
// This does not take into account how much storage did the underlying allocator (malloc/mimalloc) reserved.
size_t GetAllocatedHeapSize(ObjHeader* object) noexcept;