Summary
A dict sent across a channel from a spawned thread arrives in the
receiving thread with its entry count intact but its string keys
corrupted (garbage bytes). Lookups then miss, so msg.some_key reads null.
Same-thread send/try_recv is fine — the corruption is specific to the
cross-thread transfer. Looks like the channel hand-off doesn't deep-copy (or
re-intern) the dict's heap key strings across the thread boundary, so the
receiver reads dangling pointers into the sender thread's freed string memory.
Environment
- EigenScript
main (@ abf7e7b, 0.21.1); make build, x86-64 Linux.
Reproducer (minimal, confirmed firsthand)
define w(data) as:
send of [data.channel, {"id": 7, "total": 42}]
return null
ch is channel of null
h is spawn of [w, {"channel": ch}]
msg is null
s is 0
loop while msg == null and s < 2000:
msg is try_recv of ch
if msg == null:
usleep of 1000
s is s + 1
thread_join of h
close_channel of ch
print of ("type=" + (type of msg))
print of ("keys=" + (str of (keys of msg)) + " len=" + (str of (len of msg)))
print of ("total=" + (str of msg.total))
Output:
type=dict
keys=["<2 binary-garbage strings>"] len=2 # count right, key strings corrupt
total=null # lookup misses -> null
Control (same thread, no spawn) is correct:
ch is channel of null
send of [ch, {"id": 7, "total": 42}]
msg is try_recv of ch
# keys=["id","total"] total=42
Diagnosis
The received dict has the right entry count but garbage key strings → the
channel transfer moves the dict's structure but not a thread-safe copy of its
heap key strings; the receiver dereferences pointers into memory the sender
thread has freed/reused. Likely also affects heap string values (the repro
uses num values, which are immediate and survive); worth checking strings as
values, and nested lists/dicts.
Impact
Any dict (or string-bearing structure) passed between threads via a channel is
silently corrupted — a real hazard for the worker→main result pattern. Surfaced
by EigenGauntlet's concurrent stress lab (cannot apply '+' to num and null).
Single-threaded use is unaffected.
Suggested fix
On cross-thread channel send, deep-copy the value into the receiver's
ownership (intern key strings + copy string/heap payloads) rather than handing
over pointers owned by the sender thread — mirroring whatever spawn-arg passing
does correctly (spawn args reach the worker intact).
Summary
A dict sent across a
channelfrom a spawned thread arrives in thereceiving thread with its entry count intact but its string keys
corrupted (garbage bytes). Lookups then miss, so
msg.some_keyreadsnull.Same-thread
send/try_recvis fine — the corruption is specific to thecross-thread transfer. Looks like the channel hand-off doesn't deep-copy (or
re-intern) the dict's heap key strings across the thread boundary, so the
receiver reads dangling pointers into the sender thread's freed string memory.
Environment
main(@abf7e7b, 0.21.1);make build, x86-64 Linux.Reproducer (minimal, confirmed firsthand)
Output:
Control (same thread, no spawn) is correct:
Diagnosis
The received dict has the right entry count but garbage key strings → the
channel transfer moves the dict's structure but not a thread-safe copy of its
heap key strings; the receiver dereferences pointers into memory the sender
thread has freed/reused. Likely also affects heap string values (the repro
uses num values, which are immediate and survive); worth checking strings as
values, and nested lists/dicts.
Impact
Any dict (or string-bearing structure) passed between threads via a channel is
silently corrupted — a real hazard for the worker→main result pattern. Surfaced
by EigenGauntlet's
concurrentstress lab (cannot apply '+' to num and null).Single-threaded use is unaffected.
Suggested fix
On cross-thread channel
send, deep-copy the value into the receiver'sownership (intern key strings + copy string/heap payloads) rather than handing
over pointers owned by the sender thread — mirroring whatever spawn-arg passing
does correctly (spawn args reach the worker intact).