diff --git a/include/silver/engine.h b/include/silver/engine.h index 1966d21..2928c6b 100644 --- a/include/silver/engine.h +++ b/include/silver/engine.h @@ -383,11 +383,20 @@ static inline bool sv_frame_is_strict(const sv_frame_t *frame) { return frame && frame->func && frame->func->is_strict; } +static inline bool sv_slot_in_range( + const ant_value_t *base, size_t count, + const ant_value_t *slot +) { + if (!base || !slot || count == 0) return false; + + uintptr_t lo = (uintptr_t)base; + uintptr_t hi = lo + count * sizeof(*base); + uintptr_t addr = (uintptr_t)slot; + return addr >= lo && addr < hi; +} + static inline bool sv_slot_in_vm_stack(const sv_vm_t *vm, const ant_value_t *slot) { - return - vm && vm->stack && slot && - slot >= vm->stack && - slot < vm->stack + vm->stack_size; + return vm && sv_slot_in_range(vm->stack, (size_t)vm->stack_size, slot); } static inline bool sv_is_nullish_this(ant_value_t v) { diff --git a/src/silver/engine.c b/src/silver/engine.c index f084e4e..01e65d4 100644 --- a/src/silver/engine.c +++ b/src/silver/engine.c @@ -86,8 +86,7 @@ static bool sv_vm_grow_stack(sv_vm_t *vm) { } for (sv_upvalue_t *uv = vm->open_upvalues; uv; uv = uv->next) if ( uv->location != &uv->closed && - uv->location >= old && - uv->location < old + old_size + sv_slot_in_range(old, (size_t)old_size, uv->location) ) uv->location += delta; } diff --git a/src/silver/ops/exceptions.h b/src/silver/ops/exceptions.h index a703f09..d91c074 100644 --- a/src/silver/ops/exceptions.h +++ b/src/silver/ops/exceptions.h @@ -27,7 +27,7 @@ static inline void sv_close_upvalues_from_slot(sv_vm_t *vm, ant_value_t *slot) { while (*pp) { sv_upvalue_t *uv = *pp; ant_value_t *loc = uv->location; - if (loc >= slot && sv_slot_in_vm_stack(vm, loc)) { + if (sv_slot_in_vm_stack(vm, loc) && loc >= slot) { uv->closed = *loc; uv->location = &uv->closed; *pp = uv->next; diff --git a/src/silver/ops/upvalues.h b/src/silver/ops/upvalues.h index ccbbba2..d6c4cb5 100644 --- a/src/silver/ops/upvalues.h +++ b/src/silver/ops/upvalues.h @@ -62,14 +62,15 @@ static inline void sv_op_close_upval(sv_vm_t *vm, sv_frame_t *frame, uint8_t *ip sv_upvalue_t **pp = &vm->open_upvalues; while (*pp) { - sv_upvalue_t *uv = *pp; - if (uv->location >= slot) { - uv->closed = *uv->location; - uv->location = &uv->closed; - *pp = uv->next; - } else pp = &uv->next; + sv_upvalue_t *uv = *pp; + ant_value_t *loc = uv->location; + if (sv_slot_in_vm_stack(vm, loc) && loc >= slot) { + uv->closed = *loc; + uv->location = &uv->closed; + *pp = uv->next; } -} + else pp = &uv->next; +}} static inline sv_upvalue_t *sv_capture_upvalue(sv_vm_t *vm, ant_value_t *slot) { sv_upvalue_t **pp = &vm->open_upvalues; diff --git a/tests/repro_interp_close_on_jit_upvalues.cjs b/tests/repro_interp_close_on_jit_upvalues.cjs new file mode 100644 index 0000000..ffe3cb4 --- /dev/null +++ b/tests/repro_interp_close_on_jit_upvalues.cjs @@ -0,0 +1,79 @@ +// Repro for mixed-mode upvalue corruption: +// a hot child closure bails out to the interpreter, then executes OP_CLOSE_UPVAL +// while its hot parent frame still has open captured locals in JIT storage. + +function makeHandle(prefix) { + return function handle(seed, triggerBailout) { + let count = seed + 1; + const box = { prefix, seed }; + + function sibling() { + return prefix.length + count + box.seed; + } + + function next(value, bail) { + let total = prefix.length + count; + + for (let i = 0; i < 1; i++) { + let captured = 7; + const readCaptured = () => captured; + total += readCaptured(); + + // Warm numerically, then force a bailout before the loop scope closes. + if (bail) total = total + value; + } + + return total; + } + + const first = next(triggerBailout ? '!' : 1, triggerBailout); + + // If the child interpreter close path corrupts this upvalue, sibling() + // will stop observing the live parent slot after we mutate count. + count += 10; + + return { + first, + sibling: sibling(), + count, + }; + }; +} + +const handle = makeHandle('root'); + +for (let i = 0; i < 250; i++) { + const got = handle(i, false); + const want = { + first: 4 + (i + 1) + 7, + sibling: 4 + (i + 11) + i, + count: i + 11, + }; + + if ( + got.first !== want.first || + got.sibling !== want.sibling || + got.count !== want.count + ) { + throw new Error( + `warmup mismatch at ${i}: expected ${JSON.stringify(want)}, got ${JSON.stringify(got)}` + ); + } +} + +const got = handle(10, true); +const want = { + first: '22!', + sibling: 35, + count: 21, +}; + +if ( + got.first !== want.first || + got.sibling !== want.sibling || + got.count !== want.count +) { + throw new Error(`expected ${JSON.stringify(want)}, got ${JSON.stringify(got)}`); +} + +console.log('OK: bailout + interpreter close preserved parent JIT upvalues', JSON.stringify(got)); diff --git a/tests/repro_jit_mixed_upvalues_return.cjs b/tests/repro_jit_mixed_upvalues_return.cjs new file mode 100644 index 0000000..418c547 --- /dev/null +++ b/tests/repro_jit_mixed_upvalues_return.cjs @@ -0,0 +1,35 @@ +function factory(prefix) { + return function handle(n) { + let count = n + 1; + let state = { value: prefix + ':' + n }; + let items = [prefix, n]; + + function trimPrefix(x) { + return x + ':' + items[0]; + } + + function next() { + return state.value + '|' + count + '|' + trimPrefix('ok'); + } + + return next; + }; +} + +const handle = factory('root'); + +for (let i = 0; i < 150; i++) { + const fn = handle(i); + if (fn() !== `root:${i}|${i + 1}|ok:root`) { + throw new Error(`warmup mismatch at ${i}: ${fn()}`); + } +} + +const next = handle(7); +const got = next(); +const want = 'root:7|8|ok:root'; +if (got !== want) { + throw new Error(`expected ${want}, got ${got}`); +} + +console.log('OK: mixed upvalue returned closure survived', got);