Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmake/targets/BuildBun.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,12 @@ if(BUN_LINK_ONLY)
endif()

if(WIN32)
# bmalloc.lib is still linked because WTF.lib references bmalloc symbols.
# However, bmalloc's SystemHeap is unimplemented on Windows (all methods
# crash), so BmallocSystemHeapOverride.cpp provides a working implementation
# using mimalloc. Combined with setting Malloc=1 at startup (main.zig), this
# bypasses libpas for all allocations.
# See: https://github.com/oven-sh/bun/issues/22349
if(DEBUG)
target_link_libraries(${bun} PRIVATE
${WEBKIT_LIB_PATH}/WTF.lib
Expand Down
286 changes: 286 additions & 0 deletions src/bun.js/bindings/BmallocSystemHeapOverride.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
/**
* Working SystemHeap implementation for Windows using mimalloc.
*
* bmalloc's SystemHeap is unimplemented on Windows — all methods crash with
* RELEASE_BASSERT_NOT_REACHED. This file provides a functional replacement
* that uses mimalloc for allocations.
*
* Combined with setting the "Malloc" environment variable at process startup,
* this causes bmalloc/libpas to redirect all allocations through SystemHeap
* instead of using libpas directly, avoiding GC crashes on Windows.
*
* See: https://github.com/oven-sh/bun/issues/22349
* https://github.com/oven-sh/bun/issues/21569
*
* How it works:
* 1. Bun sets Malloc=1 env var early in main() (see src/main.zig)
* 2. bmalloc's Environment reads Malloc=1 and enables SystemHeap
* 3. All bmalloc allocations route through SystemHeap
* 4. This file provides SystemHeap using mimalloc instead of crashing
*
* Link-time override:
* This object file defines the same symbols as bmalloc.lib's
* SystemHeap.cpp.o. Since object files are processed before static
* libraries, the linker uses these definitions and skips bmalloc.lib's
* unimplemented stubs.
*/

#include <bmalloc/BPlatform.h>

#if BOS(WINDOWS)

// We are providing the implementation of bmalloc's SystemHeap, so we need
// BEXPORT to resolve to __declspec(dllexport) rather than dllimport.
#define BUILDING_bmalloc

#include <bmalloc/SystemHeap.h>
#include <bmalloc/Algorithm.h>
#include <bmalloc/BAssert.h>
#include <bmalloc/VMAllocate.h>
#include "mimalloc.h"

#if BENABLE(LIBPAS)
#include <bmalloc/pas_system_heap.h>
#endif
Comment on lines +42 to +44

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "BmallocSystemHeapOverride.cpp" -type f

Repository: oven-sh/bun

Length of output: 106


🏁 Script executed:

find . -name "SystemHeap.cpp" -type f

Repository: oven-sh/bun

Length of output: 37


🏁 Script executed:

wc -l ./src/bun.js/bindings/BmallocSystemHeapOverride.cpp

Repository: oven-sh/bun

Length of output: 110


🏁 Script executed:

cat -n ./src/bun.js/bindings/BmallocSystemHeapOverride.cpp

Repository: oven-sh/bun

Length of output: 8808


🌐 Web query:

site:raw.githubusercontent.com WebKit SystemHeap.cpp pas_system_heap_malloc PAS_PROFILE

💡 Result:

In WebKit’s Source/bmalloc/bmalloc/SystemHeap.cpp (commit 1034e0634fd9bc7f1413cea076d0536b0e7cf87c), the libpas glue defines pas_system_heap_malloc() and instruments it with PAS_PROFILE before delegating to SystemHeap:

  • pas_system_heap_malloc(size_t size):
    • gets auto systemHeap = SystemHeap::getExisting();
    • calls PAS_PROFILE(SYSTEM_HEAP_ALLOCATION, systemHeap, size, 0, pas_non_compact_allocation_mode);
    • returns systemHeap->malloc(size, FailureAction::ReturnNull); [1]

The corresponding declarations live in Source/bmalloc/libpas/src/libpas/pas_system_heap.h (same commit), declaring pas_system_heap_malloc, pas_system_heap_memalign, etc., as BEXPORT extern ... when PAS_BMALLOC is enabled. [2]

Sources: [1], [2]


🌐 Web query:

site:raw.githubusercontent.com WebKit SystemHeap.cpp PAS_MTE_HANDLE

💡 Result:

In WebKit’s repo, PAS_MTE_HANDLE appears in Source/bmalloc/bmalloc/SystemHeap.cpp (the pas_system_heap_* allocation wrappers), e.g. these functions call it:

  • pas_system_heap_malloc
  • pas_system_heap_memalign
  • pas_system_heap_realloc
  • pas_system_heap_malloc_compact
  • pas_system_heap_memalign_compact
  • pas_system_heap_realloc_compact [1]

Raw files:

https://raw.githubusercontent.com/WebKit/WebKit/main/Source/bmalloc/bmalloc/SystemHeap.cpp
https://raw.githubusercontent.com/WebKit/WebKit/main/Source/bmalloc/bmalloc/SystemHeap.h
```  [1][2]

**Sources:** [1] [2]

---



</details>

**Mirror the PAS_PROFILE and PAS_MTE_HANDLE instrumentation from WebKit's canonical SystemHeap.cpp.**

This override currently acts as a symbol-only replacement without the profiling/tagging behavioral parity. WebKit's `SystemHeap.cpp` wraps all `pas_system_heap_*` allocations with `PAS_PROFILE(SYSTEM_HEAP_ALLOCATION, ...)` and `PAS_MTE_HANDLE` before delegating. On Windows LIBPAS builds, dropping these hooks means profiling and tagging data is incomplete.

Add `#include <bmalloc/pas_mte.h>` at line 39, then wrap the allocation calls in all six functions (`pas_system_heap_malloc`, `memalign`, `realloc`, and their `_compact` variants at lines 174–207) with both profiling hooks before the `SystemHeap::getExisting()->` delegation to maintain instrumentation consistency.

<details>
<summary>♻️ Suggested parity patch</summary>

```diff
 `#if` BENABLE(LIBPAS)
+#include <bmalloc/pas_mte.h>
 `#include` <bmalloc/pas_system_heap.h>
 `#endif`
@@
 void* pas_system_heap_malloc(size_t size)
 {
-    return SystemHeap::getExisting()->malloc(size, FailureAction::ReturnNull);
+    auto systemHeap = SystemHeap::getExisting();
+    PAS_PROFILE(SYSTEM_HEAP_ALLOCATION, systemHeap, size, 0, pas_non_compact_allocation_mode);
+    PAS_MTE_HANDLE(SYSTEM_HEAP_ALLOCATION, systemHeap, size, 0, pas_non_compact_allocation_mode);
+    return systemHeap->malloc(size, FailureAction::ReturnNull);
 }

Apply the same pattern to memalign, realloc, and their *_compact variants.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun.js/bindings/BmallocSystemHeapOverride.cpp` around lines 38 - 40, Add
instrumentation parity with WebKit by including <bmalloc/pas_mte.h> and wrapping
all pas_system_heap allocation calls with PAS_PROFILE(SYSTEM_HEAP_ALLOCATION,
...) and PAS_MTE_HANDLE before delegating to SystemHeap::getExisting()->...;
specifically update the functions pas_system_heap_malloc,
pas_system_heap_memalign, pas_system_heap_realloc and their
pas_system_heap_*_compact variants so each allocation/deallocation call is
wrapped with PAS_PROFILE and PAS_MTE_HANDLE to restore profiling/tagging
behavior.


namespace bmalloc {

SystemHeap* systemHeapCache { nullptr };

BALLOW_DEPRECATED_DECLARATIONS_BEGIN
DEFINE_STATIC_PER_PROCESS_STORAGE(SystemHeap);
BALLOW_DEPRECATED_DECLARATIONS_END

SystemHeap::SystemHeap(const LockHolder&)
: m_pageSize(vmPageSize())
{
}

void* SystemHeap::malloc(size_t size, FailureAction action)
{
void* result = mi_malloc(size);
RELEASE_BASSERT(action == FailureAction::ReturnNull || result);
return result;
}

void* SystemHeap::memalign(size_t alignment, size_t size, FailureAction action)
{
void* result = mi_malloc_aligned(size, alignment);
RELEASE_BASSERT(action == FailureAction::ReturnNull || result);
return result;
}

void* SystemHeap::realloc(void* object, size_t size, FailureAction action)
{
void* result = mi_realloc(object, size);
RELEASE_BASSERT(action == FailureAction::ReturnNull || result);
return result;
}

void SystemHeap::free(void* object)
{
mi_free(object);
}

void SystemHeap::scavenge()
{
mi_collect(false);
}

void SystemHeap::dump()
{
}

void* SystemHeap::memalignLarge(size_t alignment, size_t size)
{
alignment = roundUpToMultipleOf(m_pageSize, alignment);
size = roundUpToMultipleOf(m_pageSize, size);
void* result = tryVMAllocate(alignment, size);
if (!result)
return nullptr;
{
LockHolder locker(mutex());
m_sizeMap[result] = size;
}
return result;
}

void SystemHeap::freeLarge(void* base)
{
if (!base)
return;

size_t size;
{
LockHolder locker(mutex());
size = m_sizeMap[base];
size_t numErased = m_sizeMap.erase(base);
RELEASE_BASSERT(numErased == 1);
Comment on lines +108 to +118

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

header="$(fd 'SystemHeap\.h$' . | head -n1)"
test -n "$header"
echo "== inspecting ${header} =="
rg -n -C5 '\bclass SystemHeap\b|\bm_sizeMap\b|\btake\s*\(|\bfind\s*\(|\berase\s*\(' "$header"

Repository: oven-sh/bun

Length of output: 37


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# First, let's check the .cpp file directly
cpp_file="src/bun.js/bindings/BmallocSystemHeapOverride.cpp"
if [ ! -f "$cpp_file" ]; then
    echo "File not found at expected location"
    exit 1
fi

echo "=== Examining the .cpp file for includes and m_sizeMap usage ==="
head -n 150 "$cpp_file"

Repository: oven-sh/bun

Length of output: 4000


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for SystemHeap.h in the repository
echo "=== Looking for SystemHeap.h ==="
find . -name "SystemHeap.h" -type f 2>/dev/null | head -20

# Also search for bmalloc headers
echo -e "\n=== Looking for bmalloc directory ==="
find . -type d -name "bmalloc" 2>/dev/null | head -10

Repository: oven-sh/bun

Length of output: 126


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any bmalloc references or comments
echo "=== Searching for bmalloc references in the repo ==="
rg -l "bmalloc" --type-list 2>/dev/null | head -10

# Look for any comments or documentation mentioning SystemHeap
echo -e "\n=== Searching for SystemHeap references ==="
rg "SystemHeap" --type-list 2>/dev/null | head -5

# Search within the cpp file for m_sizeMap or map container hints
echo -e "\n=== Searching for map/HashMap patterns in bindings directory ==="
fd "\.h$|\.cpp$" src/bun.js/bindings/ | xargs rg "sizeMap|HashMap|map<" 2>/dev/null | head -20

Repository: oven-sh/bun

Length of output: 107


🌐 Web query:

bmalloc SystemHeap m_sizeMap container type find method

💡 Result:

In WebKit’s bmalloc::SystemHeap, m_sizeMap is a C++ standard hash table:

  • Container type: std::unordered_map<void*, size_t> m_sizeMap; (maps the base pointer of a “large” VM allocation to its size). [1]

To look up an entry, use std::unordered_map::find:

auto it = m_sizeMap.find(base);
if (it != m_sizeMap.end()) {
    size_t size = it->second;
}

(Internally, the code also uses erase(base) when freeing a large allocation.) [2]

Sources:
[1] WebKit SystemHeap.h (declaration of m_sizeMap)
[2] WebKit SystemHeap.cpp (use of m_sizeMap for large alloc/free)


Use find() instead of operator[] to avoid silent insertion on lookup miss.

m_sizeMap[base] creates a default entry {base, 0} if base is not in the map. This means if a large allocation was never tracked (bug elsewhere), erase(base) still succeeds and returns 1, the assertion passes, but size is 0. This causes vmDeallocate(base, 0) to be called with an incorrect size, leading to memory corruption.

Replace with:

auto it = m_sizeMap.find(base);
RELEASE_BASSERT(it != m_sizeMap.end());
size_t size = it->second;
m_sizeMap.erase(it);

This ensures the allocation was actually tracked before proceeding.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun.js/bindings/BmallocSystemHeapOverride.cpp` around lines 108 - 118, In
SystemHeap::freeLarge, using m_sizeMap[base] silently inserts a zero-sized entry
on misses causing vmDeallocate(base, 0) and potential corruption; change the
lookup to use m_sizeMap.find(base), RELEASE_BASSERT that the iterator is not
end(), read size from it->second, then erase the entry via m_sizeMap.erase(it)
before calling vmDeallocate; ensure RELEASE_BASSERT references the find result
so we only deallocate tracked allocations.

}

vmDeallocate(base, size);
}

SystemHeap* SystemHeap::tryGetSlow()
{
SystemHeap* result;
if (Environment::get()->isSystemHeapEnabled()) {
systemHeapCache = SystemHeap::get();
result = systemHeapCache;
} else {
systemHeapCache = systemHeapDisabled();
result = nullptr;
}
RELEASE_BASSERT(systemHeapCache);
return result;
}

} // namespace bmalloc

#if BENABLE(LIBPAS)

#if BUSE(LIBPAS)

using namespace bmalloc;

bool pas_system_heap_is_enabled(pas_heap_config_kind kind)
{
switch (kind) {
case pas_heap_config_kind_bmalloc:
return !!SystemHeap::tryGet();
case pas_heap_config_kind_jit:
case pas_heap_config_kind_pas_utility:
return false;
default:
BCRASH();
return false;
}
}

bool pas_system_heap_should_supplant_bmalloc(pas_heap_config_kind kind)
{
SystemHeap* heap;
switch (kind) {
case pas_heap_config_kind_bmalloc:
heap = SystemHeap::tryGet();
if (!heap)
return false;
return heap->shouldSupplantBmalloc();
case pas_heap_config_kind_jit:
case pas_heap_config_kind_pas_utility:
return false;
default:
BCRASH();
return false;
}
}

void* pas_system_heap_malloc(size_t size)
{
return SystemHeap::getExisting()->malloc(size, FailureAction::ReturnNull);
}

void* pas_system_heap_memalign(size_t alignment, size_t size)
{
return SystemHeap::getExisting()->memalign(alignment, size, FailureAction::ReturnNull);
}

void* pas_system_heap_realloc(void* ptr, size_t size)
{
return SystemHeap::getExisting()->realloc(ptr, size, FailureAction::ReturnNull);
}

void pas_system_heap_free(void* ptr)
{
SystemHeap::getExisting()->free(ptr);
}

void* pas_system_heap_malloc_compact(size_t size)
{
return SystemHeap::getExisting()->malloc(size, FailureAction::ReturnNull);
}

void* pas_system_heap_memalign_compact(size_t alignment, size_t size)
{
return SystemHeap::getExisting()->memalign(alignment, size, FailureAction::ReturnNull);
}

void* pas_system_heap_realloc_compact(void* ptr, size_t size)
{
return SystemHeap::getExisting()->realloc(ptr, size, FailureAction::ReturnNull);
}

#else // !BUSE(LIBPAS)

bool pas_system_heap_is_enabled(pas_heap_config_kind kind)
{
BUNUSED_PARAM(kind);
return false;
}

bool pas_system_heap_should_supplant_bmalloc(pas_heap_config_kind kind)
{
BUNUSED_PARAM(kind);
return false;
}

void* pas_system_heap_malloc(size_t size)
{
BUNUSED_PARAM(size);
RELEASE_BASSERT_NOT_REACHED();
return nullptr;
}

void* pas_system_heap_memalign(size_t alignment, size_t size)
{
BUNUSED_PARAM(size);
BUNUSED_PARAM(alignment);
RELEASE_BASSERT_NOT_REACHED();
return nullptr;
}

void* pas_system_heap_realloc(void* ptr, size_t size)
{
BUNUSED_PARAM(ptr);
BUNUSED_PARAM(size);
RELEASE_BASSERT_NOT_REACHED();
return nullptr;
}

void* pas_system_heap_malloc_compact(size_t size)
{
BUNUSED_PARAM(size);
RELEASE_BASSERT_NOT_REACHED();
return nullptr;
}

void* pas_system_heap_memalign_compact(size_t alignment, size_t size)
{
BUNUSED_PARAM(size);
BUNUSED_PARAM(alignment);
RELEASE_BASSERT_NOT_REACHED();
return nullptr;
}

void* pas_system_heap_realloc_compact(void* ptr, size_t size)
{
BUNUSED_PARAM(ptr);
BUNUSED_PARAM(size);
RELEASE_BASSERT_NOT_REACHED();
return nullptr;
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wmissing-noreturn"
void pas_system_heap_free(void* ptr)
{
BUNUSED_PARAM(ptr);
RELEASE_BASSERT_NOT_REACHED();
}
#pragma clang diagnostic pop

#endif // BUSE(LIBPAS)

#endif // BENABLE(LIBPAS)

#endif // BOS(WINDOWS)
1 change: 1 addition & 0 deletions src/c-headers-for-zig.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
#if WINDOWS
#include <windows.h>
#include <winternl.h>
#include <stdlib.h> // _putenv
#endif

#undef lstat
Expand Down
14 changes: 14 additions & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ pub extern "c" var environ: ?*anyopaque;
pub fn main() void {
_bun.crash_handler.init();

// Force bmalloc to use SystemHeap (mimalloc) instead of libpas on Windows.
// bmalloc's libpas allocator causes GC crashes under memory pressure on
// Windows because SystemHeap is unimplemented (all methods assert-crash).
// Setting "Malloc" causes bmalloc::Environment to route all allocations
// through SystemHeap, which we override with mimalloc
// (see BmallocSystemHeapOverride.cpp). Must be set before any bmalloc
// allocation triggers Environment initialization.
// See: https://github.com/oven-sh/bun/issues/22349
if (Environment.isWindows) {
// _putenv returns 0 on success, -1 on failure. Failure here means
// bmalloc will use unimplemented libpas and crash later, so assert.
_bun.assert(_bun.c._putenv("Malloc=1") == 0);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment on lines +32 to +36

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The _putenv("Malloc=1") call added to main.zig (line 33) to force bmalloc to route all allocations through SystemHeap/mimalloc is missing from the test entry point src/main_test.zig. The test binary's Windows init block (lines 25-34) has the same uv_replace_allocator and environ setup but lacks this call, so the test build won't get the full libpas bypass on Windows. Add _ = bun.c._putenv("Malloc=1"); before the uv_replace_allocator call in main_test.zig.

Extended reasoning...

What the bug is

This PR adds _putenv("Malloc=1") to src/main.zig to force bmalloc to use SystemHeap (backed by mimalloc) instead of libpas on Windows. However, Bun has two entry points: src/main.zig for the main binary and src/main_test.zig for the test binary (bun-*-test). The fix was only applied to the main entry point.

The specific code path

In main_test.zig:22-34, the main() function has a Windows initialization block that calls uv_replace_allocator and sets up environ/_environ, mirroring the same block in main.zig. The new _putenv("Malloc=1") call was added to main.zig before this block but was not replicated in main_test.zig.

Why existing code doesn't prevent it

The two entry points are completely separate files with duplicated initialization logic. Changes to one are not automatically reflected in the other. The PR description and commit only modified main.zig, leaving main_test.zig untouched.

Impact

Without Malloc=1 being set, bmalloc::Environment::isSystemHeapEnabled() returns false in the test binary, so libpas remains the primary allocator rather than being bypassed through SystemHeap/mimalloc. The crash itself is still prevented because BmallocSystemHeapOverride.cpp provides working SystemHeap implementations (so when SystemHeap IS invoked during GC, it uses mimalloc instead of crashing). However, the test binary does not get the full allocation routing through mimalloc that the PR intends.

Step-by-step proof

  1. Build the test binary on Windows (cmake with -DTEST=ON), which uses main_test.zig as the entry point.
  2. The test binary starts, enters main() at main_test.zig:22.
  3. The Windows block at line 25 runs uv_replace_allocator and sets environ, but never calls _putenv("Malloc=1").
  4. When bmalloc's Environment initializes, it checks for the Malloc env var, finds it unset, and does NOT enable SystemHeap for all allocations.
  5. libpas remains the primary allocator in the test binary, unlike the main binary where Malloc=1 causes all allocations to route through SystemHeap/mimalloc.

How to fix

Add _ = bun.c._putenv("Malloc=1"); at the beginning of the Windows block in main_test.zig, before the uv_replace_allocator call, matching what was done in main.zig.


if (Environment.isPosix) {
var act: std.posix.Sigaction = .{
.handler = .{ .handler = std.posix.SIG.IGN },
Expand Down
Loading