Repository navigation
Conversation
|
That feels too heavy. The gc will only destroy unreachable islands, I think it might be enough to both push a ref to the container under stringification onto the stack (keeps it reachable) and mark it constant (avoids deletions/other mutations during stringification). Then, after tostring(), restore the original constness (value might've been constant already so do not unconditionally revert), then pop the value ref from stack & put it |
The stringification code relies on its argument staying alive and
unchanged while it runs, but a tostring callback executes arbitrary
script code. When the callback triggers a collection, the mark phase
stops at the container already marked for cycle detection, its children
get freed while the formatter still iterates them and the sweep clears
the mark, defeating the cycle detection for the rest of the traversal.
When the callback unlinks the value from its parent and throws, the
reference held by the call frame is dropped during unwinding and the
formatter continues with freed memory. When it deletes the next key of
an object being formatted, the iteration visits a freed hash table
entry.
Push a reference to each array, object or resource being stringified
onto the VM stack to keep it reachable and alive across callbacks. Mark
containers constant for the duration of their traversal so that
callbacks cannot add, remove or replace their entries, restoring the
original constness afterwards, and register them in a chain of stack
frames in uc_thread_context_t. The collector clears the cycle detection
marks of the registered containers before its mark phase and restores
them after the sweep, so cycle detection keeps using the mark bit at
constant cost per value. Formatting without a VM registers nothing since
no callbacks can run, and once exit() has been called no further
callbacks are invoked, since the stack reset performed on exit also
discards the formatter's slot.
Add regressions for explicit and automatic collection, arrays and
objects, reentrant formatting with cycles and shared values, rejected
mutations of containers under stringification, converter exceptions,
exit() in a converter, converters modifying their own value, formatting
of constant values and collection of unreachable cycles in a callback.
Fixes `ucode -p 'let a; a = [ proto({}, { tostring: () => { gc(); return "A"; } }), { b: 7 } ]; a'`
crashing.
Suggested-by: Jo-Philipp Wich <jo@mein.io>
Signed-off-by: darekhta <dmitri.arekhta@me.com>
530fdb2 to
ac50ea4
Compare
|
Tried your variant first: pushing the container onto the stack and constifying it works for the mutation cases, but the gc-in-tostring cases still crash. The culprit is the cycle detection mark: So I kept your approach and only added two small loops to the collector: clear the marks of the containers currently being stringified before the mark phase, restore them after the sweep. Containers are tracked through a chain of C stack frames hanging off One deliberate deviation: the container is made constant for the traversal of its children, not during its own tostring(), so a converter can still cache into |
|
Ah right, because tostring's cycle detection and gc's reachability walk both share the sole mark bit. Let me deliberate for a while to figure out whether your proposal is the best possible solution - but thank you so far for trying my suggestion. |
|
Counter-proposal in #488. It fixes the same bug with a different mechanism: instead of threading a per-container frame through the C stack and hooking it into the collector, it gives the collector a dedicated Consequences:
The reproducer from the description is fixed and the full custom suite passes, plus a new regression test ( Happy to discuss the trade-offs; the two approaches put the complexity in quite different places (GC-side bookkeeping vs. a bit + a self-contained formatter). |
|
Superseded by #488. |
The stringification code relies on its argument staying alive and unchanged while it runs, but a tostring callback executes arbitrary script code. When the callback triggers a collection, the mark phase stops at the container already marked for cycle detection, its children get freed while the formatter still iterates them and the sweep clears the mark, defeating the cycle detection for the rest of the traversal. When the callback unlinks the value from its parent and throws, the reference held by the call frame is dropped during unwinding and the formatter continues with freed memory. When it deletes the next key of an object being formatted, the iteration visits a freed hash table entry.
Push a reference to each array, object or resource being stringified onto the VM stack to keep it reachable and alive across callbacks. Mark containers constant for the duration of their traversal so that callbacks cannot add, remove or replace their entries, restoring the original constness afterwards, and register them in a chain of stack frames in
uc_thread_context_t. The collector clears the cycle detection marks of the registered containers before its mark phase and restores them after the sweep, so cycle detection keeps using the mark bit at constant cost per value. Formatting without a VM registers nothing since no callbacks can run, and once exit() has been called no furthercallbacks are invoked, since the stack reset performed on exit also discards the formatter's slot.
Add regressions for explicit and automatic collection, arrays and objects, reentrant formatting with cycles and shared values, rejected mutations of containers under stringification, converter exceptions, exit() in a converter, converters modifying their own value, formatting of constant values and collection of unreachable cycles in a callback.
Fixes
ucode -p 'let a; a = [ proto({}, { tostring: () => { gc(); return "A"; } }), { b: 7 } ]; a'crashing.