Skip to content

Make WithHostState actually allocate state per instantiation #5

Description

@JairusSW

Problem

WithHostState documents that its value is created once per Instantiate call and lives exactly as long as that instance, but its API accepts an already-created value and stores that same value every time the Option is applied:

func WithHostState(key, value any) Option {
    return func(c *config) {
        if c.hostState == nil {
            c.hostState = map[any]any{}
        }
        c.hostState[key] = value
    }
}

Relevant code:

// whatever the rep names.
func WithHostResourceDtor(tag uint32, fn func(ctx context.Context, rep uint32) error) Option {
return withHostResourceDtor(tag, fn)
}
// WithHostState attaches an opaque value to every Instance built with this
// option, retrievable via Instance.HostState(key). It is how a host
// implementation of a stateful interface keeps per-instance state: the value
// is created once per Instantiate call and lives exactly as long as the
// Instance.
//
// key should be a package-private type, not a bare string, so two independent
// host implementations cannot collide.
func WithHostState(key, value any) Option {
return func(c *config) {
if c.hostState == nil {
c.hostState = map[any]any{}
}

Functional options are naturally reusable, and this package explicitly supports repeated Instantiate calls with shared options such as WithCompileCache. Reusing an option containing WithHostState(key, ptr) therefore attaches the same pointer to every instance rather than creating per-instance state.

Impact

Stateful hosts can accidentally share mutable resource maps and request state across isolated component instances. This is not only a documentation mismatch: wago-org/wasi/p2.WithWASI returns reusable-looking []component.Option values that capture one filesystem host, socket host, poll host, and HTTP host. Reusing that slice for two instantiations causes both instances to share those objects; their resource hooks also overwrite the captured handle-table pointer with the most recently instantiated table.

That permits cross-instance state corruption, races, stale-handle resolution against the wrong table, and unintended resource visibility between instances.

Suggested fix

Provide a factory form, for example:

func WithHostStateFactory(key any, newState func() any) Option

and run the factory while building each instantiation's config. Stateful host packages also need a way to build a coherent bundle of imports, resource hooks, destructors, and host state from the same fresh per-instance object—either an option-factory abstraction or an option whose application allocates the bundle and registers all associated callbacks into that config.

The existing WithHostState(key, value) could remain as an explicitly shared-state primitive, but its documentation should no longer claim per-instance creation.

Regression test

Create one option value, reuse it for two Instantiate calls, and assert:

  • HostState(key) points to distinct objects;
  • mutating one instance's host state cannot be observed from the other;
  • each WithResourcesHook-backed host resolves handles only through its own instance's table.

A companion WASI test should reuse opts := wasip2.With(cfg) across two instances and verify that filesystem/HTTP/socket reps and drops remain isolated.

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