blob: db7731f57fea8573a2601cfcf7a32d252ebe5df4 [file] [log] [blame]
/*
* Copyright (C) 2016-2017 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "WasmMemory.h"
#if ENABLE(WEBASSEMBLY)
#include "VM.h"
#include "WasmThunks.h"
#include <wtf/Gigacage.h>
#include <wtf/Lock.h>
#include <wtf/Platform.h>
#include <wtf/PrintStream.h>
#include <wtf/RAMSize.h>
namespace JSC { namespace Wasm {
// FIXME: We could be smarter about memset / mmap / madvise. https://bugs.webkit.org/show_bug.cgi?id=170343
// FIXME: Give up some of the cached fast memories if the GC determines it's easy to get them back, and they haven't been used in a while. https://bugs.webkit.org/show_bug.cgi?id=170773
// FIXME: Limit slow memory size. https://bugs.webkit.org/show_bug.cgi?id=170825
namespace {
constexpr bool verbose = false;
NEVER_INLINE NO_RETURN_DUE_TO_CRASH void webAssemblyCouldntGetFastMemory() { CRASH(); }
struct MemoryResult {
enum Kind {
Success,
SuccessAndAsyncGC,
SyncGCAndRetry
};
static const char* toString(Kind kind)
{
switch (kind) {
case Success:
return "Success";
case SuccessAndAsyncGC:
return "SuccessAndAsyncGC";
case SyncGCAndRetry:
return "SyncGCAndRetry";
}
RELEASE_ASSERT_NOT_REACHED();
return nullptr;
}
MemoryResult() { }
MemoryResult(void* basePtr, Kind kind)
: basePtr(basePtr)
, kind(kind)
{
}
void dump(PrintStream& out) const
{
out.print("{basePtr = ", RawPointer(basePtr), ", kind = ", toString(kind), "}");
}
void* basePtr;
Kind kind;
};
class MemoryManager {
public:
MemoryManager()
: m_maxCount(Options::maxNumWebAssemblyFastMemories())
{
}
MemoryResult tryAllocateVirtualPages()
{
MemoryResult result = [&] {
auto holder = holdLock(m_lock);
if (m_memories.size() >= m_maxCount)
return MemoryResult(nullptr, MemoryResult::SyncGCAndRetry);
void* result = Gigacage::tryAllocateVirtualPages(Memory::fastMappedBytes());
if (!result)
return MemoryResult(nullptr, MemoryResult::SyncGCAndRetry);
m_memories.append(result);
return MemoryResult(
result,
m_memories.size() >= m_maxCount / 2 ? MemoryResult::SuccessAndAsyncGC : MemoryResult::Success);
}();
if (Options::logWebAssemblyMemory())
dataLog("Allocated virtual: ", result, "; state: ", *this, "\n");
return result;
}
void freeVirtualPages(void* basePtr)
{
{
auto holder = holdLock(m_lock);
Gigacage::freeVirtualPages(basePtr, Memory::fastMappedBytes());
m_memories.removeFirst(basePtr);
}
if (Options::logWebAssemblyMemory())
dataLog("Freed virtual; state: ", *this, "\n");
}
bool containsAddress(void* address)
{
// NOTE: This can be called from a signal handler, but only after we proved that we're in JIT code.
auto holder = holdLock(m_lock);
for (void* memory : m_memories) {
char* start = static_cast<char*>(memory);
if (start <= address && address <= start + Memory::fastMappedBytes())
return true;
}
return false;
}
// FIXME: Ideally, bmalloc would have this kind of mechanism. Then, we would just forward to that
// mechanism here.
MemoryResult::Kind tryAllocatePhysicalBytes(size_t bytes)
{
MemoryResult::Kind result = [&] {
auto holder = holdLock(m_lock);
if (m_physicalBytes + bytes > ramSize())
return MemoryResult::SyncGCAndRetry;
m_physicalBytes += bytes;
if (m_physicalBytes >= ramSize() / 2)
return MemoryResult::SuccessAndAsyncGC;
return MemoryResult::Success;
}();
if (Options::logWebAssemblyMemory())
dataLog("Allocated physical: ", bytes, ", ", MemoryResult::toString(result), "; state: ", *this, "\n");
return result;
}
void freePhysicalBytes(size_t bytes)
{
{
auto holder = holdLock(m_lock);
m_physicalBytes -= bytes;
}
if (Options::logWebAssemblyMemory())
dataLog("Freed physical: ", bytes, "; state: ", *this, "\n");
}
void dump(PrintStream& out) const
{
out.print("memories = ", m_memories.size(), "/", m_maxCount, ", bytes = ", m_physicalBytes, "/", ramSize());
}
private:
Lock m_lock;
unsigned m_maxCount { 0 };
Vector<void*> m_memories;
size_t m_physicalBytes { 0 };
};
static MemoryManager& memoryManager()
{
static std::once_flag onceFlag;
static MemoryManager* manager;
std::call_once(
onceFlag,
[] {
manager = new MemoryManager();
});
return *manager;
}
template<typename Func>
bool tryAndGC(VM& vm, const Func& allocate)
{
unsigned numTries = 2;
bool done = false;
for (unsigned i = 0; i < numTries && !done; ++i) {
switch (allocate()) {
case MemoryResult::Success:
done = true;
break;
case MemoryResult::SuccessAndAsyncGC:
vm.heap.collectAsync(CollectionScope::Full);
done = true;
break;
case MemoryResult::SyncGCAndRetry:
if (i + 1 == numTries)
break;
vm.heap.collectSync(CollectionScope::Full);
break;
}
}
return done;
}
} // anonymous namespace
const char* makeString(MemoryMode mode)
{
switch (mode) {
case MemoryMode::BoundsChecking: return "BoundsChecking";
case MemoryMode::Signaling: return "Signaling";
}
RELEASE_ASSERT_NOT_REACHED();
return "";
}
Memory::Memory(PageCount initial, PageCount maximum)
: m_initial(initial)
, m_maximum(maximum)
{
ASSERT(!initial.bytes());
ASSERT(m_mode == MemoryMode::BoundsChecking);
dataLogLnIf(verbose, "Memory::Memory allocating ", *this);
}
Memory::Memory(void* memory, PageCount initial, PageCount maximum, size_t mappedCapacity, MemoryMode mode)
: m_memory(memory)
, m_size(initial.bytes())
, m_initial(initial)
, m_maximum(maximum)
, m_mappedCapacity(mappedCapacity)
, m_mode(mode)
{
dataLogLnIf(verbose, "Memory::Memory allocating ", *this);
}
RefPtr<Memory> Memory::create(VM& vm, PageCount initial, PageCount maximum)
{
ASSERT(initial);
RELEASE_ASSERT(!maximum || maximum >= initial); // This should be guaranteed by our caller.
const size_t initialBytes = initial.bytes();
const size_t maximumBytes = maximum ? maximum.bytes() : 0;
// We need to be sure we have a stub prior to running code.
if (UNLIKELY(!Thunks::singleton().stub(throwExceptionFromWasmThunkGenerator)))
return nullptr;
if (maximum && !maximumBytes) {
// User specified a zero maximum, initial size must also be zero.
RELEASE_ASSERT(!initialBytes);
return adoptRef(new Memory(initial, maximum));
}
bool done = tryAndGC(
vm,
[&] () -> MemoryResult::Kind {
return memoryManager().tryAllocatePhysicalBytes(initialBytes);
});
if (!done)
return nullptr;
char* fastMemory = nullptr;
if (Options::useWebAssemblyFastMemory()) {
tryAndGC(
vm,
[&] () -> MemoryResult::Kind {
auto result = memoryManager().tryAllocateVirtualPages();
fastMemory = bitwise_cast<char*>(result.basePtr);
return result.kind;
});
}
if (fastMemory) {
bool writable = true;
bool executable = false;
OSAllocator::commit(fastMemory, initialBytes, writable, executable);
if (mprotect(fastMemory + initialBytes, Memory::fastMappedBytes() - initialBytes, PROT_NONE)) {
dataLog("mprotect failed: ", strerror(errno), "\n");
RELEASE_ASSERT_NOT_REACHED();
}
memset(fastMemory, 0, initialBytes);
return adoptRef(new Memory(fastMemory, initial, maximum, Memory::fastMappedBytes(), MemoryMode::Signaling));
}
if (UNLIKELY(Options::crashIfWebAssemblyCantFastMemory()))
webAssemblyCouldntGetFastMemory();
if (!initialBytes)
return adoptRef(new Memory(initial, maximum));
void* slowMemory = Gigacage::tryAlignedMalloc(WTF::pageSize(), initialBytes);
if (!slowMemory) {
memoryManager().freePhysicalBytes(initialBytes);
return nullptr;
}
memset(slowMemory, 0, initialBytes);
return adoptRef(new Memory(slowMemory, initial, maximum, initialBytes, MemoryMode::BoundsChecking));
}
Memory::~Memory()
{
if (m_memory) {
memoryManager().freePhysicalBytes(m_size);
switch (m_mode) {
case MemoryMode::Signaling:
mprotect(m_memory, Memory::fastMappedBytes(), PROT_READ | PROT_WRITE);
memoryManager().freeVirtualPages(m_memory);
break;
case MemoryMode::BoundsChecking:
Gigacage::alignedFree(m_memory);
break;
}
}
}
size_t Memory::fastMappedRedzoneBytes()
{
return static_cast<size_t>(PageCount::pageSize) * Options::webAssemblyFastMemoryRedzonePages();
}
size_t Memory::fastMappedBytes()
{
static_assert(sizeof(uint64_t) == sizeof(size_t), "We rely on allowing the maximum size of Memory we map to be 2^32 + redzone which is larger than fits in a 32-bit integer that we'd pass to mprotect if this didn't hold.");
return static_cast<size_t>(std::numeric_limits<uint32_t>::max()) + fastMappedRedzoneBytes();
}
bool Memory::addressIsInActiveFastMemory(void* address)
{
return memoryManager().containsAddress(address);
}
bool Memory::grow(VM& vm, PageCount newSize)
{
RELEASE_ASSERT(newSize > PageCount::fromBytes(m_size));
dataLogLnIf(verbose, "Memory::grow to ", newSize, " from ", *this);
if (maximum() && newSize > maximum())
return false;
size_t desiredSize = newSize.bytes();
RELEASE_ASSERT(desiredSize > m_size);
size_t extraBytes = desiredSize - m_size;
RELEASE_ASSERT(extraBytes);
bool success = tryAndGC(
vm,
[&] () -> MemoryResult::Kind {
return memoryManager().tryAllocatePhysicalBytes(extraBytes);
});
if (!success)
return false;
switch (mode()) {
case MemoryMode::BoundsChecking: {
RELEASE_ASSERT(maximum().bytes() != 0);
void* newMemory = Gigacage::tryAlignedMalloc(WTF::pageSize(), desiredSize);
if (!newMemory)
return false;
memcpy(newMemory, m_memory, m_size);
memset(static_cast<char*>(newMemory) + m_size, 0, desiredSize - m_size);
if (m_memory)
Gigacage::alignedFree(m_memory);
m_memory = newMemory;
m_mappedCapacity = desiredSize;
m_size = desiredSize;
return true;
}
case MemoryMode::Signaling: {
RELEASE_ASSERT(m_memory);
// Signaling memory must have been pre-allocated virtually.
uint8_t* startAddress = static_cast<uint8_t*>(m_memory) + m_size;
dataLogLnIf(verbose, "Marking WebAssembly memory's ", RawPointer(m_memory), " as read+write in range [", RawPointer(startAddress), ", ", RawPointer(startAddress + extraBytes), ")");
if (mprotect(startAddress, extraBytes, PROT_READ | PROT_WRITE)) {
dataLogLnIf(verbose, "Memory::grow in-place failed ", *this);
return false;
}
memset(startAddress, 0, extraBytes);
m_size = desiredSize;
return true;
} }
RELEASE_ASSERT_NOT_REACHED();
return false;
}
void Memory::dump(PrintStream& out) const
{
out.print("Memory at ", RawPointer(m_memory), ", size ", m_size, "B capacity ", m_mappedCapacity, "B, initial ", m_initial, " maximum ", m_maximum, " mode ", makeString(m_mode));
}
} // namespace JSC
} // namespace Wasm
#endif // ENABLE(WEBASSEMBLY)