Skip to content

Cross-thread channel transfer corrupts dict string keys (use-after-free) #293

Description

@InauguralPhysicist

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions