|
return err |
|
} |
|
|
|
// invokeEntered is invoke's actual call body, split out purely for |
|
// readability at the poisoning boundary (see the two explicit in.poisoned = |
|
// true sites below, at the ONLY two points this export's own guest code |
|
// actually runs: be.coreFn.CallWithStack and be.postReturnFn.CallWithStack). |
|
// |
|
// Deliberately NOT poisoning on every error here (an earlier version used a |
|
// single defer to poison on ANY non-nil err, which is closer to the |
|
// reference's literal try/except scope around the whole call but proved too |
|
// broad in practice: real_resource_test.go's TestRealResource calls |
|
// [method]counter.get on a handle it just dropped, EXPECTS that one call to |
|
// fail (lowerParams -> resolveArgHandles rejects the stale handle before |
|
// core code ever runs), and then keeps calling OTHER, still-valid handles on |
|
// the SAME instance -- broad poisoning permanently broke every later call. |
|
// A host-side ABI/argument validation failure (lowerParams, resolveArgHandles, |
|
// the coreArgs-count static check, liftResult) never actually enters guest |
|
// code, so -- unlike a real trap escaping a CallWithStack -- it must not |
|
// poison; matches builtin-trap-poisons-instance's own two poisoning cases |
|
// (an `unreachable` and a busy-stream host-builtin trap), both of which |
|
// surface AS be.coreFn.CallWithStack failing, so this narrower rule still |
|
// covers everything that suite (or the spec) requires here. |
|
func (in *Instance) invokeEntered(ctx context.Context, be *boundExport, exportName string, args []abi.Value) ([]abi.Value, error) { |
|
if in.syncTaskNeeded { |
|
// The reference's canon_lift constructs a Task for EVERY call, |
|
// including a not-opts.async_ sync lift (definitions.py:2144-2202); |
|
// current_task() always resolves (:315-316). Install/restore around |
|
// the whole body (before lowerParams: a guest cabi_realloc invoked |
|
// during lowering may legally call context.get) so a nested |
|
// guest->guest sync call on this same Instance |
|
// (invokeEntered -> delegatingHostImport.fn -> invoke -> |
|
// invokeEntered) still resolves the innermost task, and the caller's |
|
// task is restored on return -- including through the two poisoning |
|
// error returns below, via defer. |
|
t := &task{inst: in, be: be, state: taskStarted, syncImplicit: true} |
|
prevActive, prevBase := in.activeTask, in.syncBase |
|
in.activeTask, in.syncBase = t, t |
|
defer func() { in.activeTask, in.syncBase = prevActive, prevBase }() |
|
} |
|
mem, memAvailable := memoryBytesOf(be.mod) |
|
realloc := cachedReallocOf(ctx, be) |
|
|
|
// coreArgsPtr's buffer, like stack below, is pure scratch local to this |
|
// call (lowerParams only ever appends into it; nothing downstream of |
|
// invoke retains it), so it's fetched from the pool empty (len 0, cap |
|
// from a prior call) and handed to lowerParams to append into directly, |
|
// instead of lowerParams allocating its own backing array. |
|
coreArgsPtr := coreValueSlicePool.Get().(*[]abi.CoreValue) |
|
*coreArgsPtr = (*coreArgsPtr)[:0] |
|
coreArgs, err := in.lowerParams(be, args, mem, memAvailable, realloc, exportName, *coreArgsPtr) |
|
if err != nil { |
|
coreValueSlicePool.Put(coreArgsPtr) |
|
return nil, err |
|
} |
|
*coreArgsPtr = coreArgs |
|
if len(coreArgs) != len(be.coreParamsWant) { |
|
putCoreValueSlice(coreArgsPtr) |
|
return nil, fmt.Errorf("component/instance: export %q: parameter list flattens to %d core value(s) but the core signature expects %d; whole-parameter-list spilling to memory is not supported by this milestone", exportName, len(coreArgs), len(be.coreParamsWant)) |
|
} |
|
|
|
// stack is pure scratch: it only exists to hand coreArgs' bits to |
|
// be.coreFn.Call as a []uint64, and the native engine's callEngine.Call |
|
// copies params into its own buffer before doing anything else (see |
Problem
Instance.invokeEnteredcan execute guest code while lowering string/list/spilled arguments through the canonical ABI realloc function, but an error from that path returns without poisoning the component instance.Current flow:
invokeEnteredbuildsrealloc := cachedReallocOf(ctx, be).lowerParamsmay call the guest's configuredreallocwhile writing arguments into guest memory.Only failures from
be.coreFn.CallWithStackandbe.postReturnFn.CallWithStacksetin.poisoned = true. The nearby comment therefore incorrectly treats those as the only two places guest code runs; the code immediately above also notes that guestcabi_realloccan run during lowering.Relevant code:
component-model/internal/instance/instance.go
Lines 1642 to 1705 in 8a61139
Impact
A component can expose an export taking a string/list (or otherwise requiring memory lowering), have its guest realloc trap, and then remain enterable. A later export call proceeds even though a guest trap escaped the instance. That violates the same sticky Store-poisoning invariant enforced for the main core call and post-return.
This is distinct from host-side argument validation failures: stale handles, malformed values, and flattening errors should remain non-poisoning. Only a failure after actually entering the guest realloc needs to poison.
Suggested fix
Route all guest core-function execution through one helper that marks the owning instance poisoned when a guest call traps, including:
post-return;realloccalls made during lowering.Alternatively, make the realloc adapter return a distinguishable guest-execution error and set
in.poisonedbefore propagating it.Regression test
Add a component with:
string;unreachable;After the first call traps during argument lowering, invoking the second export should fail with
cannot enter component instancerather than entering guest code.