Skip to content

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

Description

@khagankhan

A reference stored in a global is not a GC root: freed while still reachable

Version: master, commit 8849867 (2026-09-23), Linux x64, default build
(WALRUS_MODE=release, WALRUS_GC=ON). Interpreter and --jit both affected.

Reproduce

(module
  (type $s (struct (field (mut i32))))
  (type $a (array (mut i32)))
  (global $g (mut (ref null $s)) (ref.null $s))
  (func (export "run") (result i32) (local $i i32)
    (global.set $g (struct.new $s (i32.const 305419896)))
    (loop $l                                   ;; allocate ~200 MB of garbage
      (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))))
(assert_return (invoke "run") (i32.const 305419896))
$ walrus --enable-web-assembly3 repro.wast
Assertion failed at 2: run() expected 305419896, but got 0

V8, SpiderMonkey and JavaScriptCore return 305419896. With collection
disabled (GC_DONT_GC=1 walrus ...) Walrus returns 305419896 too, so the
semantics are right and the collector is freeing a live object.

The same object parked in a local, a table slot, a struct field, a ref array
or an element segment survives. Only global.set at run time loses it.

Cause

Global::createGlobal in src/runtime/Global.h does new Global(value, type).
That is plain malloc, which bdwgc does not scan, so the Value held by the
Global is invisible to the marker and its object is collected at the next
collection. Table and Instance allocate their reference slots with
GC_MALLOC_UNCOLLECTABLE for exactly this reason (Table.cpp:59,
Instance.cpp:46); Global does not.

A global initialised by a constant expression only appears to work because
the fresh object is still on the C stack or in a register during the first
collections.

Impact

Any program that keeps a struct or array only in a global across an
allocation-heavy phase reads freed memory. On larger generated modules we see
wrong values that differ from run to run, and once the block is reused, a
SIGSEGV in the JIT inside bdwgc's finalizer walk.

Fix that works for us

Allocate Global in collector-visible memory, e.g. an operator new on
GC_MALLOC_UNCOLLECTABLE (and operator delete on GC_FREE) under
ENABLE_GC.

Ack

Will be updated

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions