Accessing a nested empty map from JavaScript via Object.keys or for...in crashes the entire BEAM process with a C assertion failure.
Reproduction
{:ok, rt} = QuickBEAM.start()
QuickBEAM.load_module(rt, "t", "globalThis.t = function(obj) { return Object.keys(obj.x).length; };")
QuickBEAM.call(rt, "t", [%{x: %{}}]) # crashes BEAM
beam.smp: quickjs.c:1626: void *js_malloc_rt(JSRuntime *, size_t): Assertion `size != 0' failed.
Aborted (core dumped)
Note: top-level empty maps work fine (QuickBEAM.call(rt, "t", [%{}])). The crash only occurs when the empty map is accessed as a nested property of another BEAM proxy object.
Root cause
beam_proxy.get_own_property_names in beam_proxy.zig:103-105 computes byte_size = total * @sizeOf(JSPropertyEnum) where total = map_size + override_count. For an empty map with no overrides, byte_size = 0, and js_malloc(ctx, 0) hits assert(size != 0) in QuickJS.
Top-level empty maps don't crash because QuickJS's JS_GetOwnPropertyNamesInternal uses max_int(atom_count, 1) before allocating. But when the BEAM proxy's exotic get_own_property_names handler is invoked through a nested property access path, it bypasses that guard.
Impact
This crashes any application using QuickBEAM for Vue SSR (via live_vue) when components receive empty maps as props — a common case (e.g. empty unread counts, empty reactions).
Environment
- QuickBEAM 0.10.3
- OTP 28.3.2
- Elixir 1.19.5
Accessing a nested empty map from JavaScript via
Object.keysorfor...incrashes the entire BEAM process with a C assertion failure.Reproduction
Note: top-level empty maps work fine (
QuickBEAM.call(rt, "t", [%{}])). The crash only occurs when the empty map is accessed as a nested property of another BEAM proxy object.Root cause
beam_proxy.get_own_property_namesinbeam_proxy.zig:103-105computesbyte_size = total * @sizeOf(JSPropertyEnum)wheretotal = map_size + override_count. For an empty map with no overrides,byte_size = 0, andjs_malloc(ctx, 0)hitsassert(size != 0)in QuickJS.Top-level empty maps don't crash because QuickJS's
JS_GetOwnPropertyNamesInternalusesmax_int(atom_count, 1)before allocating. But when the BEAM proxy's exoticget_own_property_nameshandler is invoked through a nested property access path, it bypasses that guard.Impact
This crashes any application using QuickBEAM for Vue SSR (via live_vue) when components receive empty maps as props — a common case (e.g. empty unread counts, empty reactions).
Environment