-
Notifications
You must be signed in to change notification settings - Fork 3
Description
Problem
If the ctx
used in DialContext
is cancelled AFTER the function returns, the returned water.Conn
will be closed.
See https://github.com/hwh33/water-context-bug for an example.
Cause
Two causes contributed to this bug risk.
WATER reuse the ctx
from DialContext
in all calls to WebAssembly-exported functions
Configured the water.Core
with the ctx
passed to DialContext
:
Lines 60 to 83 in d4faf1b
func (d *Dialer) DialContext(ctx context.Context, network, address string) (conn water.Conn, err error) { | |
if d.config == nil { | |
return nil, fmt.Errorf("water: dialing with nil config is not allowed") | |
} | |
ctxReady, dialReady := context.WithCancel(context.Background()) | |
go func() { | |
defer dialReady() | |
var core water.Core | |
core, err = water.NewCoreWithContext(ctx, d.config) | |
if err != nil { | |
return | |
} | |
conn, err = dial(core, network, address) | |
}() | |
select { | |
case <-ctx.Done(): | |
return nil, ctx.Err() | |
case <-ctxReady.Done(): | |
return conn, err | |
} | |
} |
Then reused this ctx
when invoking every single WebAssembly-exported function
water/transport/v1/transport_module.go
Line 402 in d4faf1b
coreCtx := tm.Core().Context() |
_init
:
water/transport/v1/transport_module.go
Line 422 in d4faf1b
ret, err := init.Call(coreCtx) |
_dial_fixed
(to be merged with _dial
):
water/transport/v1/transport_module.go
Line 454 in d4faf1b
ret, err := dial_fixed.Call(coreCtx, api.EncodeI32(callerFd)) |
_dial
:
water/transport/v1/transport_module.go
Line 486 in d4faf1b
ret, err := dial.Call(coreCtx, api.EncodeI32(callerFd)) |
_accept
(not with DialContext
, but would suffer similar bug risks):
water/transport/v1/transport_module.go
Line 518 in d4faf1b
ret, err := accept.Call(coreCtx, api.EncodeI32(callerFd)) |
_associate
(not with DialContext
, but would suffer similar bug risks):
water/transport/v1/transport_module.go
Line 548 in d4faf1b
ret, err := associate.Call(coreCtx) |
_ctrlpipe
:
water/transport/v1/transport_module.go
Line 604 in d4faf1b
ret, err := ctrlPipe.Call(coreCtx, api.EncodeI32(fd)) |
_start
:
water/transport/v1/transport_module.go
Line 612 in d4faf1b
ret, err := start.Call(coreCtx) |
At least some function calls, IF NOT ALL, should not be dependent on the context passed in by DialContext
.
Functions which MUST return before DialContext
can return (_init
, _dial
, _ctrlpipe
) should depend on the ctx
passed to DialContext
for no issue. However, the blocking main loop, _start
, should depend on a different context, probably context.Background()
.
WATER explicitly configured wazero
to terminate the execution when ctx
is canceled or timed out
(wazero.RuntimeConfig).WithCloseOnContextDone(true)
is invoked by default:
Lines 153 to 159 in d4faf1b
// NewWazeroRuntimeConfigFactory creates a new WazeroRuntimeConfigFactory. | |
func NewWazeroRuntimeConfigFactory() *WazeroRuntimeConfigFactory { | |
return &WazeroRuntimeConfigFactory{ | |
runtimeConfig: wazero.NewRuntimeConfig().WithCloseOnContextDone(true), | |
compilationCache: nil, | |
} | |
} |
And wazero/config.go#L151-L172 noted that:
// WithCloseOnContextDone ensures the executions of functions to be terminated under one of the following circumstances:
//
// - context.Context passed to the Call method of api.Function is canceled during execution. (i.e. ctx by context.WithCancel)
// - context.Context passed to the Call method of api.Function reaches timeout during execution. (i.e. ctx by context.WithTimeout or context.WithDeadline)
// - Close or CloseWithExitCode of api.Module is explicitly called during execution.
//
// This is especially useful when one wants to run untrusted Wasm binaries since otherwise, any invocation of
// api.Function can potentially block the corresponding Goroutine forever. Moreover, it might block the
// entire underlying OS thread which runs the api.Function call. See "Why it's safe to execute runtime-generated
// machine codes against async Goroutine preemption" section in RATIONALE.md for detail.
//
// Upon the termination of the function executions, api.Module is closed.
//
// Note that this comes with a bit of extra cost when enabled. The reason is that internally this forces
// interpreter and compiler runtimes to insert the periodical checks on the conditions above. For that reason,
// this is disabled by default.
//
// See examples in context_done_example_test.go for the end-to-end demonstrations.
//
// When the invocations of api.Function are closed due to this, sys.ExitError is raised to the callers and
// the api.Module from which the functions are derived is made closed.
WithCloseOnContextDone(bool) RuntimeConfig