mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-20 15:49:55 +00:00
wasm relay: harden WebTransport conn lifetime
Two real bugs in the WASM WebTransport conn that would only show up under conn teardown: - awaitPromise released its js.Func callbacks on ctx cancellation, so a pending datagram read/write promise that later settled (closing the WebTransport rejects every in-flight promise) tried to invoke a released Go function and crashed the WASM module. The callbacks now release themselves exactly once from inside the settlement path; ctx cancellation only releases the Go-side waiter. - Read and Write returned ErrClosedByServer on transport errors but didn't mark the conn closed, so the relay client's next Read could block on a fresh reader.read() promise instead of short-circuiting. Both paths now call markClosed before returning the error.
This commit is contained in:
@@ -80,6 +80,11 @@ func (c *conn) Read(b []byte) (int, error) {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
// Any other error from the datagram reader is terminal — the
|
||||
// browser closes the underlying read stream on session failure.
|
||||
// Mark the conn closed so the relay client's read loop sees a
|
||||
// consistent net.ErrClosed on its next call.
|
||||
c.markClosed()
|
||||
return 0, netErr.ErrClosedByServer
|
||||
}
|
||||
if v.Get("done").Bool() {
|
||||
@@ -92,8 +97,8 @@ func (c *conn) Read(b []byte) (int, error) {
|
||||
}
|
||||
n := val.Get("byteLength").Int()
|
||||
if n > len(b) {
|
||||
// Datagrams shouldn't exceed the relay's MaxMessageSize (8 KB) so
|
||||
// this branch is defensive — truncate rather than fail hard.
|
||||
// Datagrams shouldn't exceed the relay's MaxMessageSize (8 KB)
|
||||
// so this branch is defensive — truncate rather than fail hard.
|
||||
n = len(b)
|
||||
}
|
||||
js.CopyBytesToGo(b[:n], val)
|
||||
@@ -114,6 +119,7 @@ func (c *conn) Write(b []byte) (int, error) {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
c.markClosed()
|
||||
return 0, netErr.ErrClosedByServer
|
||||
}
|
||||
return len(b), nil
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"sync"
|
||||
"syscall/js"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -87,18 +88,31 @@ func prepareURL(address string) (string, error) {
|
||||
return parsed.String(), nil
|
||||
}
|
||||
|
||||
// awaitPromise bridges a JS Promise to a Go return. It respects ctx
|
||||
// cancellation and releases its js.Func callbacks on the resolve/reject path.
|
||||
// awaitPromise bridges a JS Promise to a Go return.
|
||||
//
|
||||
// js.Func release timing is the subtle part: if we released the callbacks on
|
||||
// ctx cancellation, the JS engine would later try to invoke a dead Go
|
||||
// function when the promise eventually settled (closing the WebTransport
|
||||
// rejects every pending read/write promise) and crash the WASM module. So
|
||||
// the callbacks always release themselves from inside the settlement path,
|
||||
// regardless of whether the Go side is still listening. Ctx cancellation
|
||||
// only releases the Go-side goroutine; the JS-side callbacks live until JS
|
||||
// runs them once.
|
||||
func awaitPromise(ctx context.Context, p js.Value) (js.Value, error) {
|
||||
type res struct {
|
||||
val js.Value
|
||||
err error
|
||||
}
|
||||
ch := make(chan res, 1)
|
||||
var thenFn, catchFn js.Func
|
||||
var (
|
||||
thenFn, catchFn js.Func
|
||||
releaseOnce sync.Once
|
||||
)
|
||||
release := func() {
|
||||
thenFn.Release()
|
||||
catchFn.Release()
|
||||
releaseOnce.Do(func() {
|
||||
thenFn.Release()
|
||||
catchFn.Release()
|
||||
})
|
||||
}
|
||||
thenFn = js.FuncOf(func(_ js.Value, args []js.Value) interface{} {
|
||||
var v js.Value
|
||||
@@ -109,6 +123,7 @@ func awaitPromise(ctx context.Context, p js.Value) (js.Value, error) {
|
||||
case ch <- res{val: v}:
|
||||
default:
|
||||
}
|
||||
release()
|
||||
return nil
|
||||
})
|
||||
catchFn = js.FuncOf(func(_ js.Value, args []js.Value) interface{} {
|
||||
@@ -120,16 +135,15 @@ func awaitPromise(ctx context.Context, p js.Value) (js.Value, error) {
|
||||
case ch <- res{err: errors.New(msg)}:
|
||||
default:
|
||||
}
|
||||
release()
|
||||
return nil
|
||||
})
|
||||
p.Call("then", thenFn).Call("catch", catchFn)
|
||||
|
||||
select {
|
||||
case r := <-ch:
|
||||
release()
|
||||
return r.val, r.err
|
||||
case <-ctx.Done():
|
||||
release()
|
||||
return js.Value{}, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user