Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 36 additions & 16 deletions mrbgems/mruby-task/src/task.c
Original file line number Diff line number Diff line change
Expand Up @@ -1787,35 +1787,55 @@ mrb_task_proc_set(mrb_state *mrb, mrb_value task, struct RProc *proc)

struct mrb_context *c = &t->c;

/* Grow the task's stack to fit the proc being set. It may need more
* registers than the original proc the stack was sized for.
* mrb_stack_extend() works on mrb->c, so point mrb->c at the task context
* across the call, and put it back through MRB_ENSURE: the extend
* allocates, and an allocation that fails raises, which would otherwise
* leave mrb->c on the task's context for whatever runs next. */
if (c->stbase && !MRB_PROC_CFUNC_P(proc) && proc->body.irep) {
size_t cur = (size_t)(c->stend - c->stbase);
size_t need = (size_t)proc->body.irep->nregs;
if (need > cur) {
struct task_stack_grow_ctx ctx = { (mrb_int)need, mrb->c };
/* Follow the alias chain to the actual target proc, just like
* mrb_proc_arity() and mrb_proc_eql() do. body.irep holds an mrb_sym
* on an alias proc and a C function on a cfunc proc, so reading it
* there is invalid. Furthermore, CI_PROC_SET refuses alias procs.
* Thus, resolve the alias before sizing and installing the proc. */
const struct RProc *rp = proc;
while (rp && MRB_PROC_ALIAS_P(rp)) {
rp = rp->upper;
}
const mrb_irep *irep = (rp && !MRB_PROC_CFUNC_P(rp)) ? rp->body.irep : NULL;

/* Grow the task's stack to fit the proc being set. The proc is installed
* on the task's current frame, so the room that matters is above
* ci->stack, not the total capacity: a task preempted mid-call-chain
* holds its frame at an offset. The check is stack_extend()'s own guard
* with an exact fit allowed, so a proc the frame already holds does not
* touch the allocator. mrb_stack_extend() works on mrb->c, so point
* mrb->c at the task context across the call, and put it back through
* MRB_ENSURE: the extend allocates, and an allocation that fails raises,
* which would otherwise leave mrb->c on the task's context for whatever
* runs next. */
if (irep && c->stbase && c->ci) {
mrb_int need = (mrb_int)irep->nregs;
if (!c->ci->stack || c->ci->stack + need > c->stend) {
struct task_stack_grow_ctx ctx = { need, mrb->c };
mrb_value result;
mrb->c = c;
MRB_ENSURE(mrb, result, task_stack_grow_body, &ctx) {
mrb->c = ctx.prev_c;
}
/* With no jmpbuf in the caller MRB_ENSURE cannot re-raise: a failed
* extend leaves mrb->exc set and control here. Keep the old proc
* rather than install one the stack cannot hold. */
if (mrb->exc) return;
}
}

/* Handle environment resize if needed */
if (t->c.cibase && t->c.cibase->u.env) {
/* Handle environment resize if needed. Widening is only valid for an
* env still backed by the task's stack. A heap env owns exactly
* MRB_ENV_LEN slots. */
if (irep && t->c.cibase && t->c.cibase->u.env) {
struct REnv *e = mrb_vm_ci_env(t->c.cibase);
if (e && MRB_ENV_LEN(e) < proc->body.irep->nlocals) {
MRB_ENV_SET_LEN(e, proc->body.irep->nlocals);
if (e && MRB_ENV_ONSTACK_P(e) && MRB_ENV_LEN(e) < irep->nlocals) {
MRB_ENV_SET_LEN(e, irep->nlocals);
}
}

if (t->c.ci) {
mrb_vm_ci_proc_set(t->c.ci, proc);
mrb_vm_ci_proc_set(t->c.ci, rp);
}
}

Expand Down
45 changes: 45 additions & 0 deletions mrbgems/mruby-task/test/proc_set_stack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,48 @@
end
end
end

# Deep-frame variant: a task suspended mid-call-chain holds its current frame
# at an offset from the stack base, and proc_set installs the proc on that
# frame. Sizing the stack by total capacity misses this case: the proc fits
# the capacity, the frame offset plus the proc does not, and the resumed task
# writes past the stack.
if Object.const_defined?(:TaskTest) && TaskTest.respond_to?(:proc_set_deep)
assert('mruby-task: proc_set sizes the stack from the frame of a preempted task') do
# Suspend a task at the bottom of a nested call chain.
nest = nil
nest = lambda do |n|
if n > 0
nest.call(n - 1)
else
sleep # suspend this task mid-chain
end
end
deep = Task.new { nest.call(8) }
begin
50.times do
break if deep.status == :SUSPENDED
TaskTest.run_once
end
assert_equal :SUSPENDED, deep.status

# ~40 locals -> nregs below TASK_STACK_INIT_SIZE (64) on its own, past it
# when placed on the suspended task's frame.
big = Proc.new {
a0=0;a1=0;a2=0;a3=0;a4=0;a5=0;a6=0;a7=0;a8=0;a9=0;a10=0;a11=0;a12=0;a13=0;a14=0;a15=0;a16=0;a17=0;a18=0;a19=0;
a20=0;a21=0;a22=0;a23=0;a24=0;a25=0;a26=0;a27=0;a28=0;a29=0;a30=0;a31=0;a32=0;a33=0;a34=0;a35=0;a36=0;a37=0;a38=0;a39=0
}
off, need, before, after = TaskTest.proc_set_deep(deep, big)
# Preconditions that make this the deep-frame case. If the call chain or
# the proc drift in size, these fail loudly instead of testing nothing.
assert_true off > 0, "suspended task should hold a deep frame (off=#{off})"
assert_true need <= before, "proc alone should fit the old capacity (need=#{need}, before=#{before})"
assert_true off + need > before, "frame + proc should exceed the old capacity (off=#{off}, need=#{need}, before=#{before})"
assert_true after >= off + need, "task stack (#{after}) must cover frame offset (#{off}) + proc nregs (#{need})"
ensure
# Drop the probe task even when something above raised. A suspended
# task left in the queue keeps a later Task.run from terminating.
deep.terminate unless deep.status == :DORMANT
end
end
end
57 changes: 57 additions & 0 deletions mrbgems/mruby-task/test/tasktest.c
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,62 @@ tasktest_extend_past_stend(mrb_state *mrb, mrb_value self)
return err ? mrb_nil_value() : size;
}

/* Deep-frame variant of the proc_set sizing regression: a task suspended
mid-call-chain holds its current frame at an offset from the stack base,
and proc_set installs the proc on that frame. The stack must be sized
from the frame offset, not the total capacity. Returns
[frame_off, replacement_nregs, slots_before, slots_after]. */
struct proc_set_deep_ctx {
mrb_value task;
struct RProc *proc;
};

static mrb_value
proc_set_deep_body(mrb_state *mrb, void *data)
{
struct proc_set_deep_ctx *ctx = (struct proc_set_deep_ctx*)data;
mrb_task_proc_set(mrb, ctx->task, ctx->proc);
return mrb_nil_value();
}

static mrb_value
tasktest_proc_set_deep(mrb_state *mrb, mrb_value self)
{
mrb_value task, big_blk;
mrb_get_args(mrb, "oo", &task, &big_blk);
if (!mrb_obj_is_kind_of(mrb, task, mrb_class_get(mrb, "Task"))) {
mrb_raise(mrb, E_TYPE_ERROR, "Task required");
}
if (mrb_type(big_blk) != MRB_TT_PROC) {
mrb_raise(mrb, E_TYPE_ERROR, "Proc required");
}
struct RProc *big = mrb_proc_ptr(big_blk);
if (MRB_PROC_CFUNC_P(big) || MRB_PROC_ALIAS_P(big) || !big->body.irep) {
mrb_raise(mrb, E_ARGUMENT_ERROR, "irep proc required");
}

mrb_task *t = (mrb_task*)DATA_PTR(task);
if (!t || !t->c.stbase || !t->c.ci) {
mrb_raise(mrb, E_ARGUMENT_ERROR, "task has no context");
}
mrb_value r[4];
r[0] = mrb_fixnum_value((mrb_int)(t->c.ci->stack - t->c.stbase));
r[1] = mrb_fixnum_value((mrb_int)big->body.irep->nregs);
r[2] = mrb_fixnum_value((mrb_int)(t->c.stend - t->c.stbase));

struct proc_set_deep_ctx ctx = { task, big };
mrb_value result;
MRB_ENSURE(mrb, result, proc_set_deep_body, &ctx) {
r[3] = mrb_fixnum_value((mrb_int)(t->c.stend - t->c.stbase));
/* Drop the probe task even when proc_set raises: a sleep-forever task
left suspended keeps a later Task.run from terminating. It is never
resumed either way, the proc was installed on a deep frame only to
measure the sizing. */
mrb_terminate_task(mrb, task);
}
return mrb_ary_new_from_values(mrb, 4, r);
}

void
mrb_mruby_task_gem_test(mrb_state* mrb)
{
Expand All @@ -225,4 +281,5 @@ mrb_mruby_task_gem_test(mrb_state* mrb)
mrb_define_module_function(mrb, tasktest, "run_sync", tasktest_run_sync, MRB_ARGS_BLOCK());
mrb_define_module_function(mrb, tasktest, "proc_set_stack", tasktest_proc_set_stack, MRB_ARGS_REQ(2));
mrb_define_module_function(mrb, tasktest, "extend_past_stend", tasktest_extend_past_stend, MRB_ARGS_REQ(3));
mrb_define_module_function(mrb, tasktest, "proc_set_deep", tasktest_proc_set_deep, MRB_ARGS_REQ(2));
}
Loading