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:
|
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
|
coreCtx := tm.Core().Context() |
_init:
|
ret, err := init.Call(coreCtx) |
_dial_fixed (to be merged with _dial):
|
ret, err := dial_fixed.Call(coreCtx, api.EncodeI32(callerFd)) |
_dial:
|
ret, err := dial.Call(coreCtx, api.EncodeI32(callerFd)) |
_accept (not with DialContext, but would suffer similar bug risks):
|
ret, err := accept.Call(coreCtx, api.EncodeI32(callerFd)) |
_associate (not with DialContext, but would suffer similar bug risks):
|
ret, err := associate.Call(coreCtx) |
_ctrlpipe:
|
ret, err := ctrlPipe.Call(coreCtx, api.EncodeI32(fd)) |
_start:
|
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:
|
// 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
Problem
If the
ctxused inDialContextis cancelled AFTER the function returns, the returnedwater.Connwill be closed.See https://github.com/hwh33/water-context-bug for an example.
Cause
Two causes contributed to this bug risk.
WATER reuse the
ctxfromDialContextin all calls to WebAssembly-exported functionsConfigured the
water.Corewith thectxpassed toDialContext:water/transport/v1/dialer.go
Lines 60 to 83 in d4faf1b
Then reused this
ctxwhen invoking every single WebAssembly-exported functionwater/transport/v1/transport_module.go
Line 402 in d4faf1b
_init:water/transport/v1/transport_module.go
Line 422 in d4faf1b
_dial_fixed(to be merged with_dial):water/transport/v1/transport_module.go
Line 454 in d4faf1b
_dial:water/transport/v1/transport_module.go
Line 486 in d4faf1b
_accept(not withDialContext, but would suffer similar bug risks):water/transport/v1/transport_module.go
Line 518 in d4faf1b
_associate(not withDialContext, but would suffer similar bug risks):water/transport/v1/transport_module.go
Line 548 in d4faf1b
_ctrlpipe:water/transport/v1/transport_module.go
Line 604 in d4faf1b
_start:water/transport/v1/transport_module.go
Line 612 in d4faf1b
At least some function calls, IF NOT ALL, should not be dependent on the context passed in byDialContext.Functions which MUST return before
DialContextcan return (_init,_dial,_ctrlpipe) should depend on thectxpassed toDialContextfor no issue. However, the blocking main loop,_start, should depend on a different context, probablycontext.Background().WATER explicitly configured
wazeroto terminate the execution whenctxis canceled or timed out(wazero.RuntimeConfig).WithCloseOnContextDone(true)is invoked by default:water/wazero_config.go
Lines 153 to 159 in d4faf1b
And wazero/config.go#L151-L172 noted that: