Skip to content

Allocate Global with uncollectable memory so GC keeps live references - #509

Open
makachanm wants to merge 2 commits into
Samsung:mainfrom
makachanm:bugfix
Open

makachanm wants to merge 2 commits into
Samsung:mainfrom
makachanm:bugfix

Conversation

@makachanm

@makachanm makachanm commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Fix #502.

@ksh8281 ksh8281 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the PR!

I have a concern about potential memory leaks due to conservative GC scanning. Since Wasm globals write arbitrary raw numbers (i32, i64, f32, etc.), allocating them as uncollectable memory will lead to severe false pointer retention (leaks) as the GC scans the entire block conservatively.

Could you please update this to use a custom mark procedure (proc) to create a GC kind and only mark/scan valid pointer slots?

Also, please update gcutil to the latest branch (bdwgc_8_3_pre_260715) for the time being.

@zherczeg

zherczeg commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

I would keep the GC object as malloced. Its value should be an union, which contains primitive values, or a pointer to a pointer for references. The referenced pointer is allocated by GC.

@clover2123

Copy link
Copy Markdown
Collaborator

@clover2123

Copy link
Copy Markdown
Collaborator

@zherczeg
Would you elaborate more about your idea?
If a GC pointer is newly allocated in a Global object, can we recognize it and just call addRef?

@zherczeg

zherczeg commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

A global type is known at allocation time, so the global type can tell that we have a primitive value or a pointer. A pointer is a pointer to a "vector", which has a single pointer value. So it is kind of a table with a single value, but this table is allocated by GC.

This way globals keeps their current form except for references when GC is enabled.

@zherczeg

zherczeg commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

Another option is having a real "global vector" for references, which is stored in the Store. This vector has used and unused slots. Releasing globals is more complex that way.

@makachanm

Copy link
Copy Markdown
Contributor Author

Is enough to use GC_MALLOC_ATOMIC_UNCOLLECTABLE?
or it needs rework about entire Global object?

@zherczeg

zherczeg commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

I am not sure I understand the question. The global object should remain mostly the same. It is a store object, and it cannot be on the webassembly heap, so it does not need to be handled by GC.

@ksh8281

ksh8281 commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

@makachanm @clover2123

Regarding the Global allocation and how we should handle the pointer tracking with bdwgc, here is my feedback on the proposed changes:

  1. If we can determine the allocation properties at allocation time:
    Case A: bdwgc needs to track the allocation itself, but the internal content does NOT contain any pointers.

In this case, using GC_MALLOC_ATOMIC_UNCOLLECTABLE is the most appropriate choice to avoid unnecessary scanning.

Case B: The internal content indeed contains pointers that the GC must traverse.

We could go with typed GC (like the one linked by clover1213).

However, since the kind value is very small here, there is practically no risk of conservative GC misinterpreting it as an arbitrary pointer. Therefore, if the Value is just a simple raw pointer, using GC_MALLOC_UNCOLLECTABLE should be more than enough.

  1. If we CANNOT 100% determine this at allocation time:
    We should introduce a new custom kind and register a marking procedure (proc) to handle this dynamically.

Similar to how it is implemented in Escargot's CustomAllocator.cpp, we should write a custom proc that evaluates the object at runtime, determines if it behaves as a pointer, and explicitly notifies bdwgc (i.e., "this is a pointer") only when it is actually identified as one.

Let me know what you think about this approach!

@zherczeg

zherczeg commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

Globals are not real objects, they are objects in Walrus only for practical reasons. They represent values that can be imported/exported by WebAssembly instances. They are stored in a Store, and they should have a reference counter, not just stored until the Store is destroyed. When a global object is created, everything is known about it, including its type. The type is important for importing/exporting checks.

A global object could be allocated by GC_MALLOC_UNCOLLECTABLE / GC_FREE, if it contains a reference, or regular malloc/free if it does not. Or it can have a pointer to a single reference. The latter increases the runtime a bit.

@makachanm

Copy link
Copy Markdown
Contributor Author

For the code consistency, GCArray already uses Value::isRefType(valueType) ? GC_MALLOC(totalSize) : GC_MALLOC_ATOMIC(totalSize) to allocate memory with type check that implies this value can't be a reference when normally allocated.

I think if the type is constant, this kind of method will prevent GC confusion.

@ksh8281

ksh8281 commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

Yes, that makes sense. Since a Global’s type is known at creation and does not change, we can choose the allocation strategy based on its type. My earlier suggestion about a custom marking procedure is unnecessary in this case.

The same type-based approach as GCArray applies, but the allocation must also respect the Global’s explicitly managed lifetime. Reference globals can use GC_MALLOC_UNCOLLECTABLE with GC_FREE, while non-reference globals can keep malloc/free. If we want both paths to use GC allocation, GC_MALLOC_ATOMIC_UNCOLLECTABLE is suitable for the non-reference case.

Comment thread src/runtime/Global.cpp
Global* Global::createGlobal(Store* store, const Value& value, const MutableType& type)
{
#ifdef ENABLE_GC
void* mem = Value::isRefType(type.type()) ? GC_MALLOC_UNCOLLECTABLE(sizeof(Global)) : GC_MALLOC_ATOMIC_UNCOLLECTABLE(sizeof(Global));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is GC_MALLOC_ATOMIC_UNCOLLECTABLE is a regular malloc? Is it recommended instead of a malloc?

@makachanm makachanm Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

GC_MALLOC_ATOMIC_UNCOLLECTABLE is uncollectable GC allocation.

Issue #502 is caused by early collection of global object that originally written in new allocation.

Marking uncollctable keeps object safe.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That is clear to me. My question is GC_MALLOC_ATOMIC_UNCOLLECTABLE is better than regular malloc? Why?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Using regular free(or delete) is possible, but it needs separate memory deallocation path into two ways.

I just think that is not a smooth way for consistency.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ok. I hope these blocks are not difficult to process for GC.

Comment thread src/runtime/Global.h Outdated

#ifdef ENABLE_GC
void operator delete(void* ptr);
#endif

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Longer term task: Store objects should be freed only by Store, so we might need to make their free operations private. And we should also add a reference counting.

(drop (array.new_default $a (i32.const 65536)))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br_if $l (i32.lt_u (local.get $i) (i32.const 3000))))
(struct.get $s 0 (global.get $g))))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do you need this large constant? Is this a long running test?

Maybe we should add a walrus_gc builtin rather than these large consts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The manual call of builtin seems makes false positive. (I'm currently testing it. Just added builtin function.) We need mess up the memory to reproduce this bug.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That is very strange. You need a short loop to remove the reference from the stack, but the referenced object should go away.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@zherczeg
IMO this code is enough to test the correct GC for the global object.
In the loop, arrays are repeatedly allocated and dropped to trigger the GC.
Even more, an explicit GC call is invoked.
So, this code could confirm that GC tracking is correctly executed for the global object.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you for explaining. My only worry is that 305419896 might make the test long running, and there is no guarantee that GC is triggered depending on the available memory. So I prefer a forced GC. A small loop should be enough to clear the stack from the earlier references, and GC should free those objects. However, @makachanm said it does not work, and I asked why.

Comment thread src/runtime/Global.cpp Outdated

#ifdef ENABLE_GC
#include "GCUtil.h"
#include <new>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this include <new> necessary?

@makachanm
makachanm force-pushed the bugfix branch 2 times, most recently from 009cbd2 to b8b54c7 Compare October 7, 2026 08:19
@makachanm

makachanm commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Do I have more to do for this patch? (Is there are something I missing about?)

Comment thread src/shell/Shell.cpp Outdated
ft,
[](ExecutionState& state, Value* argv, Value* result, void* data) {
#ifdef ENABLE_GC
GC_gcollect();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would you change this function to GC_gcollect_and_unmap();?
This gc function is more powerful because it additionally returns the reclaimed memory to OS

Comment thread src/shell/Shell.cpp Outdated
[](ExecutionState& state, Value* argv, Value* result, void* data) {
},
nullptr));
} else if (import->fieldName() == "gc") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would prefer walrus_gc, because the others are spec test definitions, and if they introduce a gc in the future, it would be good to detect it. It is unlikely that they introduce walrus_gc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

got it.

@zherczeg zherczeg left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM. Only the loop of 305419896 is strange for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] GC: references held in globals are not roots and get freed while live

4 participants