Skip to content

Add the ability for threads to manually set their "current task" #659

Open

Description

@lukewagner

With Preview 3 concurrency, at all execution points, there is a well-defined async call stack that terminates at a call from the host into a component export. This async call stack is useful for runtime debugging/profiling/tracing scenarios but it also allows a host-implemented import to have a well-defined answer to "which host export call am I executing on behalf of" which is otherwise hard to answer when you're executing multiple concurrent tasks. For example, this information is useful for logging and proxy loop detection scenarios in WASI HTTP. In the future, the same information could be exposed to wasm, for virtualizaiton purposes, when a parent component donut wraps a child component.

One nice thing about the async call stack is that it is maintained automatically by default (even with cooperative threads) and so in many scenarios the import-to-export attribution will just work. However, if the guest code / guest runtime has its own in-wasm concurrency runtime/scheduler, it's possible that all component export calls end up funneling down to a single scheduler loop in which case the default async call stack will make it look like everything is coming from one root host call. At this point, there's little we can do automatically without some degree of help from the guest code.

One option is that we could provide a new CM built-in that allows the guest's concurrency runtime / scheduler to explicitly override the default-propagated async call stack with something better, based on the guest's own internal bookkeeping state. The simplest way I can think of to do this would be to add a built-in (say thread.set-task):

(func $thread.set-task (param $other-thread-index i32))

This would change the current thread's associated async call stack to be the same as some other thread. (In the CABI Python code, this would mostly just be current_thread().task = current_instance().threads[other-thread-index].task.) As background: each call to a component export mints a new i32 thread index (retrieved by that thread via thread.index). So then the guest concurrency runtime could record the thread.indexes of all new export calls and, using whatever guest-internal bookkeeping, call thread.set-task right before executing a work-item from the shared scheduler loop so that the scheduler thread executes "on behalf of" that work-item's associated export call.

With the addition of this built-in, we could also non-normatively document an expectation (like we did with the Component Instance lifetime section) that guest toolchains should ensure (via automatic propagation or manual thread.set-task) that import calls are "properly" attributed to export calls ("properly" in quotes b/c what that means is language-dependent). It's important to note that there's no way to formally define or enforce this expectation, so it's a cooperative, best-effort sort of thing and thus should not be used for security-sensitive discrimination between concurrent async export calls (which, if a component instance is being reused, would be a bad idea in any case). Logging and accident-mitigation are good use cases, though.

This all seems additive to what's already defined/implemented, so I don't think adding this built-in blocks the initial release of Preview 3, but it does seem like a good follow-up worth considering.

(I'm going to be on vacation for the next 2 weeks after tomorrow, so apologies in advance for slow replies.)

Activity

  1. dicej commented on Jun 5, 2026

    @dicej
    Collaborator

    This sounds generally reasonable to me.

    To add some context regarding real-world guest toolchains: componentize-go and componentize-py both currently generate components such that each instantiation has a global "soup" of coroutines which are run opportunistically without differentiating between CM-level tasks. That means that, until this feature is implemented and those tools are updated to use it, the "default" attribution of import calls to tasks will be unreliable for components. It also means that, assuming we ship WASIp3 without this feature, we'll have components in the wild which for which attribution is unreliable with no obvious way of detecting them.

    For Go, which by conscious design does not have any notion of goroutine-local ambient storage, and instead relies on explicitly-passed Context parameters to thread this kind of state within and across goroutines, I expect we'll update wit-bindgen-go such that every export and import takes a Context parameter and calls thread.set-task prior to calling any import if the current task doesn't happen to match the one in the Context.

    Python has a similar model, except it's a sort of hybrid task-local storage and context parameter such that context variables are sometimes passed explicitly as parameters and sometimes are attached implicitly to coroutines as ambient state.

    I recall that someone proposed that we should proactively update toolchains such as componentize-go and componentize-py now to apply backpressure by default and thereby prevent concurrent reuse of such components, allowing only serial reuse (i.e. only unset backpressure once all tasks have exited). Personally, I think that's overkill since those components are perfectly compatible with concurrent reuse; they're just not compatible with concurrent reuse + reliable task/import call attribution, which may or may not be a concern for a given embedder. Perhaps there's a middle ground here such that we can indicate the latter without preventing the former? Or perhaps that's an argument in favor of making this feature a blocker for WASIp3 after all.

    Finally, I'll note that I think we should revisit this for WASI 1.0 and consider switching from an implicit model (where the host uses the call stack to do attribution by default and the guest can optionally override that) to an explicit model (where either the guest must specify attribution explicitly by passing the task ID as part of each import call or the guest may specify attribution and if it doesn't, the host should consider the call "unattributed"). An explicit approach would serve as a clear signal to guest toolchain implementers that they need to handle attribution explicitly rather than assume the host will figure it out automatically somehow. it would also potentially be more efficient for languages like Go which would otherwise need to call thread.set-task fairly often.

  2. alexcrichton commented on Jun 5, 2026

    @alexcrichton
    Collaborator

    The current proposed behavior for wasi:http/service is that hosts can expect correct attribution on behalf of guests, meaning that for componentize-{go,py} to work correctly with all hosts they'll need to turn on backpressure for the wasi:http/service world explicitly. As you point out this is a known deficiency, and there's a lot more words on WebAssembly/WASI#919 to this effect too.

    You might be interested to read over WebAssembly/WASI#919 @dicej because your WASI 1.0 vision isn't currently in alignment with the projected trajectory of this over time, aka this feature itself would be the "final piece". There's lots of comments on WebAssembly/WASI#919 about explicitly passing state within a guest specifically.

  3. dicej commented on Jun 5, 2026

    @dicej
    Collaborator

    The current proposed behavior for wasi:http/service is that hosts can expect correct attribution on behalf of guests, meaning that for componentize-{go,py} to work correctly with all hosts they'll need to turn on backpressure for the wasi:http/service world explicitly.

    Thanks for reminding me of that; I had forgotten. It's going to be awkward to add special-case handling for wasi:http/service to those tools, which are otherwise agnostic to the target world and its interfaces. I'd rather have a WIT- and CABI-level option to indicate that a function requires "strict attribution" (though that still feels like a nebulous concept to me), but perhaps it's too late for that.

    And thanks for the link to the other issue; I'll add my gripes to that if they're not already covered :)

  4. cfallin commented on Jun 11, 2026

    @cfallin

    Thanks Luke and Alex for writing this up and implementing it respectively -- very helpful!

    As seen on a companion issue (WebAssembly/WASI#919), I have had some concerns about the approach of creating a "hidden parameter" / "implicit global state" that allows attribution of outbound calls to inbound calls, rather than doing that attribution with more explicit plumbing of function parameters. Now that wasip3 is shipped, the time pressure is off, and there is more room to discuss, I want to understand your thinking on some aspects of this design to hopefully put to rest some of my concerns (or, conversely, raise them for you if their contour was not clear before).

    Background: attribution and essential correctness properties

    As background, first: attributing outbound calls to inbound calls in an instance-reused component (for example wasi-http) is an essential correctness property of the system I'm responsible for: when handling an inbound request, we need to know the precise request identity to make an outbound request (because of system architecture that we can't change), but also for other kinds of actions. For an example of the latter, there is a session-specific key-value store (per "flow") and we must get the wiring right if we expose this. This is contra some other characterizations of the issue as a "nice-to-have" (in companion issues and here): it's essential that we don't "cross the wires".

    To give a little more nuance to the risk model here: confusing two different request contexts is, at this abstraction level, merely a correctness problem, but it is the kind of correctness problem that is likely (unusually so) to lead to a security problem one level up the abstraction stack. In other words, one tenant, but handling multiple end-user flows or requests. Confusing these flows/requests at the framework level is "just" wrong data, but potentially has catastrophic consequences for an application built on the platform.

    So on the note above that

    It's important to note that there's no way to formally define or enforce this expectation, so it's a cooperative, best-effort sort of thing and thus should not be used for security-sensitive discrimination between concurrent async export calls (which, if a component instance is being reused, would be a bad idea in any case).

    I would say: a simple dataflow correctness issue at the Wasm level can become a security issue at higher levels, because one trust domain at one level can handle multiple contexts at higher levels. Any practical use of concurrent instance reuse in a system like ours requires this: potentially millions of RPS, but individual users' requests are rare (every N seconds), so separating instances along all dimensions of potential context is equivalent to saying that we will never do concurrent instance reuse. So as far as I can see, any system that provides APIs to the guest with context that is flow- or session-specific, to make practical use of concurrent instance reuse, needs a stronger system here.

    (In prior discussions there was some confusion about security boundaries, and to make it completely clear how we think: yes, the instance is the isolation boundary, still, and a malicious or buggy instance could still confuse contexts in any case. The point is designing a system where, if we want to allow a particular tenant to get performance from instance reuse, they still have a high probability of correct dataflow plumbing in the platform they sit on.)

    Correctness risk

    So given that, I have some concerns with the approach of using implicit, ambient global state for this attribution, and adding custom getters/setters to update it, as opposed to an alternative that explicitly threads context parameters through to all APIs that operate in the context of a request.

    It's well-known that mutable global state is error-prone, and in this particular example, we would need to trust a guest-language implementation to get and set contexts appropriately as it does user-space task switching.

    The evidence I've been able to find from a number of places is that runtimes get this wrong, repeatedly.

    • Our own Wasmtime implements MPK, Intel's Memory Protection Keys mechanism, to provide additional security and virtual-address-space density for Wasm memories. This mechanism requires updating the "current memory protection keys" on every fiber switch, and we have gotten it wrong several times: #7789 ("restore PKRU state when a fiber resumes"), #12910 ("mpk: Fix index used when purging a module"), leading to a defensive measure to try to consolidate the state-change (#10550: "Move where MPK management happens for async"). We have also had the related classical global-state problem of conflicting with another runtime in the same address space: #4786 (V8 and MPK). So if we try very hard and still make these mistakes, I would not expect an arbitrary guest runtime in Wasm for some hypothetical future language to do better.
    • There's a CWE class, CWE-226, for "Sensitive Information in Resource Not Removed Before Reuse", and perusing issues under that category is informative too.
    • CVE-2026-32887, in which Effect, a TypeScript framework that implements user-space fibers in JS/TS, incorrect use of AsyncLocalStorage in Node can contaminate data between contexts, allowing e.g. exfiltration of different users' sessions.
    • CVE-2026-5795 in Eclipse Jetty, incorrectly setting Java ThreadLocal variables with contexts, resulting in privilege escalation.
    • SEC-423 on the Spring framework: incorrect association of a context object to a request thread, giving some requests improper authentication.
    • As noted here, .NET ExecutionContext and manages async fibers. Though it seems to work fine now, it has a years-long history of bugs where this context is not properly set. E.g. here where it's reported that APIs that claim to "flow execution context" do not always work. So getting this right in a runtime, especially a mature one is a significant uphill battle and hard to do right "by construction".

    The evidence I'd like to see before we rely on this mechanism and enable instance reuse in our own use-cases, at least, is that we're confident that these kinds of issues won't recur in guest runtimes in Wasm. Especially given that, despite our best efforts, we haven't been able to make a similar mechanism bug-free in Wasmtime.

    In contrast, an explicit function parameter taking a resource handle addresses this risk moreso than the current proposal: it's still possible to pass the wrong function parameter, but an API that requires a function parameter makes it clear what plumbing is required, and in a sense it is a forcing function for a "set context" on every call, so one cannot forget what would have been a mutable state update.


    Basically, I see all of this as a quite complex and error-prone mechanism. I will say that I do see some upsides to the implicit approach as well: it is automatically inherited by every API, and doesn't require threading plumbing through the existing WITs. So in the end this is a design tradeoff. I just wanted to make sure the above downsides I see were fleshed out and understood in substance, not just in passing, and see what folks think. Thanks!

  5. dicej commented on Jun 11, 2026

    @dicej
    Collaborator

    This might be obvious, but I'll point it out anyway: regardless of whether, at the ABI level, there is an explicit parameter or an ambient task-local variable, a guest bindings generator is always free to convert from one to the other. For example, I anticipate the Go import bindings will have an explicit Context parameter which is populated using either the explicit component model parameter (if we end up going that route) or the task-local variable (if we go that route instead). Likewise for export bindings, where we'll pull the handle out of the Context parameter and either pass it using the explicit ABI or by setting the task-local variable. And of course a binding generator for e.g. C# might do the opposite, opting for an implicit rather than explicit approach: immediately storing to and later retrieving from an ExcecutionContext, regardless of the Component Model ABI.

    In other words, the ABI need not determine the shape of the bindings the component author sees, and authors of binding generators are usually more concerned with following the idioms of the target language than generating code that's a direct translation of the underlying ABI. Consequently, I feel this question is more about what we think ideal guest bindings should look like than what the underlying ABI should look like. Granted, the choice of ABI can influence the shape of guest bindings (and may have performance implications), but won't uniquely determine that shape.

  6. cfallin commented on Jun 11, 2026

    @cfallin

    Actually, in my comment I am concerned directly with the ABI. Indeed I agree (as we had all found consensus on) that the approaches are co-expressive, so bindgen can adapt to whatever shape needed. I am specifically concerned with correctness implications of a guest runtime missing the implicit-state setup that an ABI with a separate/manual "set task" intrinsic. The correctness risk profile of one vs. the other (as outlined in the issues I cited) is sufficiently different that I would be comfortable enabling concurrent instance reuse with one (explicit) but not the other (implicit), especially if documented as "best-effort" per above.

    And to make it very explicit, I am not that concerned with end-user guest-side APIs, nor do I see an explicit-parameter approach as having to infect many common libraries; it can very well be implicit if that's what the guest language does. I'm solely concerned with runtime correctness.

  7. dicej commented on Jun 11, 2026

    @dicej
    Collaborator

    Fair enough. I read this and thought it was referring to the guest side APIs:

    In contrast, an explicit function parameter taking a resource handle addresses this risk moreso than the current proposal: it's still possible to pass the wrong function parameter, but an API that requires a function parameter makes it clear what plumbing is required, and in a sense it is a forcing function for a "set context" on every call, so one cannot forget what would have been a mutable state update.

  8. cfallin commented on Jun 11, 2026

    @cfallin

    Ah, well, I should be more precise: it depends on how exactly a guest language uses wit-bindgen. I would hope/expect the parameter to be explicit underneath the layer where any guest-language runtime does user-space task switching; and I don't care whether it's implicit or explicit above that.

    So in Rust, it's fine for wit-bindgen to generate signatures as today. On the other hand, when wit-bindgen is used to generate signatures that the (say) C++ or Rust implementation of a higher-level language runtime calls, I would hope for a mode that reifies the parameters explicitly on import and export. That makes it hard to get wrong.

    The missing gap in my model was that I had considered the guest-language generating S-exprs or bytecode directly and internally implementing the ABI (hence, I wanted it explicit in the ABI); but with a bindgen layer underneath, we can still do the translation either way. So now that I write that out, I think I have instead a formulation for the level of safety I want in any runtime that we bless for concurrent instance reuse: I want it to handle contexts with explicit plumbing internally, rather than (say) instrumenting the fiber-switch sequence to set some global at just the right time.

    I'd also prefer for the documented uses of thread.{get,set}-task to be a little tighter: nothing like "cooperative, best-effort sort of thing" (above) should be in our correctness critical path.

    Thanks for poking deeper at this; splitting all the levels apart it's clear I don't actually need a change in the CM proper to get what our platform requires. Happy conclusion :-)

  9. dicej commented on Jun 11, 2026

    @dicej
    Collaborator

    (In prior discussions there was some confusion about security boundaries, and to make it completely clear how we think: yes, the instance is the isolation boundary, still, and a malicious or buggy instance could still confuse contexts in any case. The point is designing a system where, if we want to allow a particular tenant to get performance from instance reuse, they still have a high probability of correct dataflow plumbing in the platform they sit on.)

    Restating this: we should do what we can (e.g. in guest bindings generators and runtimes) to prevent the component author from shooting themself in the foot while acknowledging that they may find a way to do it anyway, whether deliberately or by (unlikely) accident. I.e. make it easy to do the right thing and hard to do the wrong thing. Does that sound right?

  10. cfallin commented on Jun 11, 2026

    @cfallin

    Yes exactly: rather than halfway giving up with "it's best effort" I'd like to push on correctness as far as we can so we can certify various toolchains/languages as reliably providing context usable for concurrent instance invocation.

  11. dicej commented on Jun 11, 2026

    @dicej
    Collaborator

    Yes exactly: rather than halfway giving up with "it's best effort" I'd like to push on correctness as far as we can so we can certify various toolchains/languages as reliably providing context usable for concurrent instance invocation.

    Okay, but that word "reliably" makes a me a little nervous. The guest toolchain can make doing the wrong thing hard, but, for most languages, not impossible. I could imagine a language that has no unsafe escape hatches, no global variables, no ability to do I/O except via effects/capabilities granted by the caller, etc. where we could make it literally impossible, but alas no popular language looks like that.

  12. cfallin commented on Jun 11, 2026

    @cfallin

    Sure, but again specifically I'm concerned about the runtimes ("toolchains/languages"). So it's less about composable pieces that some arbitrary language gives the user, and more about implementation strategy in some runtime that we examine. To the extent that runtimes can use explicit plumbing rather than global-state setters (with all the pitfalls documented above), we'd feel more comfortable. So for example I'd feel pretty uncomfortable if a Go runtime had a thread.task-set in seven different places in its goroutine scheduler (because of the bug shape described above) and much more comfortable if it ensured safety by construction by generating an interface that took a task context, and did a call $thread.set-task; call $import as an indivisible unit.

  13. lukewagner commented on Jun 16, 2026

    @lukewagner
    MemberAuthor

    (ok, back from vacation) Thanks for all the thoughtful discussion here; I'm glad we're all carefully considering the security implications.

    First, just to clarify the original write-up: the thread.set-task proposed above is not introducing any new state nor is this "thread.task" state "best-effort" from the perspective of guest code or (fully) global; it has a well-defined, component-local meaning that is already semantically observable and WAST-testable in 0.3.0.

    As background:

    At all points when executing in core wasm code inside a component, there is a well-defined current thread. The context.{get,set} built-ins use this "current thread" to define context.{get,set} in 0.3.0.

    In addition to the backing storage cells for context.{get,set}, the Thread object defined by the Canonical ABI contains a few other semantic fields that are enumerated and described in this section, one of which is Thread.task. Before adding cooperative threads (thread.new-indirect et al), threads:tasks were 1:1, but with thread.new-indirect, they are N:1 as described here. Independently of our discussion here, Thread.task already serves two semantically-observable purposes:

    • when the task.return built-in is called, the current thread's Thread.task says: who the value is being returned to (since there can be multiple waiting callers); which "number of outstanding borrowed handles" counter needs to be checked to be zero; which "already returned" state needs to be checked/set; and whether the options/type match.
    • when the caller cancels a task, the cancellation notification is only delivered to threads where Thread.task is the cancelled task. After cancellation is delivered, once borrowed handles are dropped and the callee wraps up anything else it wants to do, task.cancel may be called, which will again use the current thread's Thread.task to say which task is acknowledging cancellation.

    Thus Thread.task is already a well-defined concept and a central part of how async calls work in 0.3.0. Even without cooperative threads, a host runtime must maintain Thread.task and one can test the correctness of this via pure WAST tests like this one. The only thing new being proposed in this issue is the ability to modify Thread.task after Thread-creation.

    Also, even without considering WASI/#919, thread.set-task is useful if a thread running on behalf of task A wants to task.return or task.cancel for a task B. I almost included task.set-task as part of the original proposal, but it's additive and I wasn't aware of a concrete use case for it yet and we try not to add things ahead of uses. But the present discussion makes me think that it's not long before we do and thus thread.set-task might need to be added in any case.

    (Incidentally, in the spirit of EIBTI, I did actually spend some real time a while ago exploring how tasks could be explicitly plumbed everywhere via some new task-index and task-index table (thereby removing Thread.task), but afaics, although it feels theoretically cleaner, the net result is worse since guest glue code ends up doing implicit task propagation anyways, but now we've lost the observability benefits and we've placed an additional global coordination burden on guest-code which, in several other cases in our concurrency design, we've worked to avoid.)

    Given all that, in the blurb quoted above:

    It's important to note that there's no way to formally define or enforce this expectation, so it's a cooperative, best-effort sort of thing and thus should not be used for security-sensitive discrimination between concurrent async export calls (which, if a component instance is being reused, would be a bad idea in any case).

    this was referring to what the host must assume of an untrusted guest component (which is equivalently true if the guest was explicitly plumbing a context parameter from exports to imports) which was just restating the baseline understanding we reached in the middle of WASI/#919. But from the guest code (guest runtime/compiler/bindgen) perspective, everything is just as well-defined as if there were explicit plumbing.

    I hope that clears up some of the misgivings listed above, but happy to discuss further.

  14. cfallin commented on Jun 16, 2026

    @cfallin

    Thanks Luke -- this clarification does help w.r.t. understanding the mindset behind some of the tradeoffs here. I appreciate that you experimented with explicit plumbing as well. Since the main concern I had is just that the philosophy-of-design should allow this to be used in a correctness-load-bearing way rather than as a "nice to have" correlation, as long as that is addressed, we are happy with the presently proposed design. One could read (as I did!) the "best-effort" wording as permitting lower care for implementation correctness on both sides and/or deprioritizing the need as a mere stats-reporting/debugging kind of concern. As long as it is well-spec'd and well-tested (in all the relevant places) then we're happy.

  15. lukewagner commented on Jun 16, 2026

    @lukewagner
    MemberAuthor

    Thanks again for the consideration! Makes sense that the (somewhat-hastily-written) OP gives the impression that this would be underspecified.

  16. added 8 commits that reference this issue on Aug 20, 2026
    df29fc4
    0c769d0
    d20ced7
    dcf2278
    20b3c15
    36c0470
    79fa0d7
    ca53909
  17. added 5 commits that reference this issue on Sep 22, 2026
    c0eaea7
    643c39a
    78a5aa6
    828e3e6
    033e236
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions