Skip to content

fix(anytls): deadlock in session close#64

Open
artiga033 wants to merge 1 commit into
daeuniverse:mainfrom
artiga033:fix/anytls-deadlock-in-session-close
Open

fix(anytls): deadlock in session close#64
artiga033 wants to merge 1 commit into
daeuniverse:mainfrom
artiga033:fix/anytls-deadlock-in-session-close

Conversation

@artiga033

Copy link
Copy Markdown

Fixes a deadlock case in anytls.

Currently, in

func (s *session) Close() error {
if s.closed.CompareAndSwap(false, true) {
s.streamLock.Lock()
defer s.streamLock.Unlock()
for i := range s.streams {
s.streams[i].Close()
}
s.streams = make(map[uint32]*stream)
return s.conn.Close()
}
return nil
}

L212 s.streams[i].Close() calls session.removeStream, in which the streamLock is locked again and thus hangs permanently.

A unit test case to reproduce

outbound/protocol/anytls/session_test.go:

package anytls

import (
	"testing"
	"time"

	"github.com/daeuniverse/outbound/netproxy"
)

type mockConn struct {
}

// Close implements [netproxy.Conn].
func (m *mockConn) Close() error {
	println("mockConn closed")
	return nil
}

// Read implements [netproxy.Conn].
func (m *mockConn) Read(b []byte) (n int, err error) {
	println("mockConn read")
	return 0, nil
}

// SetDeadline implements [netproxy.Conn].
func (m *mockConn) SetDeadline(t time.Time) error {
	println("mockConn set deadline")
	return nil
}

// SetReadDeadline implements [netproxy.Conn].
func (m *mockConn) SetReadDeadline(t time.Time) error {
	println("mockConn set read deadline")
	return nil
}

// SetWriteDeadline implements [netproxy.Conn].
func (m *mockConn) SetWriteDeadline(t time.Time) error {
	println("mockConn set write deadline")
	return nil
}

// Write implements [netproxy.Conn].
func (m *mockConn) Write(b []byte) (n int, err error) {
	println("mockConn write")
	return len(b), nil
}

var _ netproxy.Conn = (*mockConn)(nil)

func TestCloseSession(t *testing.T) {
	sess := newSession(&mockConn{}, 0)
	_, err := sess.newStream("example.com:443")
	if err != nil {
		t.Fatalf("failed to create new stream: %v", err)
	}

	sess.Close()
}

go test -test.fullpath=true -timeout 30s -run ^TestCloseSession$ github.com/daeuniverse/outbound/protocol/anytls

panic: test timed out after 30s
	running tests:
		TestCloseSession (30s)


goroutine 21 [sync.Mutex.Lock]:
internal/sync.runtime_SemacquireMutex(0x7a391ac9bdc0?, 0x90?, 0x7f0320?)
	/usr/lib/go/src/runtime/sema.go:95 +0x25
internal/sync.(*Mutex).lockSlow(0x3fbdb00de2c0)
	/usr/lib/go/src/internal/sync/mutex.go:149 +0x15d
internal/sync.(*Mutex).Lock(...)
	/usr/lib/go/src/internal/sync/mutex.go:70
sync.(*Mutex).Lock(...)
	/usr/lib/go/src/sync/mutex.go:46
sync.(*RWMutex).Lock(0x7f0858?)
	/usr/lib/go/src/sync/rwmutex.go:150 +0x32
github.com/daeuniverse/outbound/protocol/anytls.(*session).removeStream(0x3fbdb00de2a0, 0x1)
	/redacted/outbound/protocol/anytls/session.go:95 +0x27
github.com/daeuniverse/outbound/protocol/anytls.(*stream).Close(0x4169c6?)
	/redacted/outbound/protocol/anytls/stream.go:77 +0x38
github.com/daeuniverse/outbound/protocol/anytls.(*session).Close(0x3fbdb00de2a0?)
	/redacted/outbound/protocol/anytls/session.go:212 +0xed
github.com/daeuniverse/outbound/protocol/anytls.TestCloseSession(0x3fbdb00e66c8)
	/redacted/outbound/protocol/anytls/session_test.go:58 +0x8f
testing.tRunner(0x3fbdb00e66c8, 0x6328e8)
	/usr/lib/go/src/testing/testing.go:2036 +0xea
created by testing.(*T).Run in goroutine 1
	/usr/lib/go/src/testing/testing.go:2101 +0x4c5
FAIL	github.com/daeuniverse/outbound/protocol/anytls	30.038s
FAIL

@artiga033 artiga033 requested a review from a team as a code owner March 4, 2026 15:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant