Skip to content

Commit 8cb3713

Browse files
Adam Fiskclaude
authored andcommitted
peer: fix nil-pointer panic when UPnP discovery fails
Two changes to make the failure-on-discover path actually surface its underlying error instead of crashing the IPC handler. 1. peer/peer.go's default cfg.NewForwarder wrapped portforward.NewForwarder with a bare `return portforward.NewForwarder(ctx)`. When discovery failed, that collapsed the `(*Forwarder)(nil), err` pair into a typed-nil interface — `if fwd != nil` in the deferred cleanup passed (the interface has a type), `fwd.UnmapPort(...)` dispatched to a nil receiver, and `f.mu.Lock()` panicked. The wrapper now returns a clean `nil, err` so the caller sees ErrNoPortForwarding (or whatever the discoverer returned) and the deferred cleanup short-circuits on the interface nil-check. 2. portforward.UnmapPort grew a defensive `if f == nil { return nil }` at the top. Belt-and-suspenders for any future caller that lands here through an interface and bypasses the inline nil-check — teardown should be idempotent on a nil receiver, not a panic. Reproduced live on macOS 26.x with `Share My Connection` toggled on when UPnP discovery returned ErrNoPortForwarding; the http2 IPC goroutine panicked instead of rolling back the toggle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 61ff325 commit 8cb3713

2 files changed

Lines changed: 20 additions & 1 deletion

File tree

peer/peer.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,18 @@ func NewClient(cfg Config) (*Client, error) {
101101
}
102102
if cfg.NewForwarder == nil {
103103
cfg.NewForwarder = func(ctx context.Context) (portForwarder, error) {
104-
return portforward.NewForwarder(ctx)
104+
// Explicitly return a nil interface on error — `return
105+
// portforward.NewForwarder(ctx)` collapses the (*Forwarder, error)
106+
// pair into a typed-nil interface on failure, which then panics
107+
// inside the deferred cleanup's `if fwd != nil { fwd.UnmapPort... }`
108+
// because the nil-check passes (interface has a type) but the
109+
// receiver is nil. Surfacing the underlying error here lets the
110+
// caller see ErrNoPortForwarding instead of a runtime panic.
111+
fwd, err := portforward.NewForwarder(ctx)
112+
if err != nil {
113+
return nil, err
114+
}
115+
return fwd, nil
105116
}
106117
}
107118
if cfg.BuildBoxService == nil {

portforward/portforward.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ func (f *Forwarder) MapPort(ctx context.Context, internalPort uint16, descriptio
113113
// about a router rule that's actually still live and the user would have
114114
// to wait for the UPnP lease to expire.
115115
func (f *Forwarder) UnmapPort(ctx context.Context) error {
116+
// Defensive: callers that pass a *Forwarder through an interface (see
117+
// peer.Client's portForwarder shim) can land here with f == nil if a
118+
// failed construction collapsed `(*Forwarder)(nil), err` into a
119+
// non-nil-but-typed-nil interface. A bare `f.mu.Lock()` would panic;
120+
// this guard makes the cleanup path idempotent against that race.
121+
if f == nil {
122+
return nil
123+
}
116124
f.mu.Lock()
117125
defer f.mu.Unlock()
118126
if f.cancel != nil {

0 commit comments

Comments
 (0)