diff --git a/CHANGELOG.md b/CHANGELOG.md index 39976aa..c63f414 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ Notable changes to Compa, newest first. Versions follow - `compa-kernel mcp add`, `mcp remove` and `mcp edit` save their change when a server has its own `call_timeout_seconds`; they refused every change. +- On Windows computers whose processor has AMX, such as recent Intel Xeon, + `compa-kernel` could crash a while after Telegram connected, with + `fatal error: found pointer to free object`. ## 3.0.0 - 2026-10-06 diff --git a/go.mod b/go.mod index 46a48fa..59d7e09 100644 --- a/go.mod +++ b/go.mod @@ -111,7 +111,7 @@ require ( github.com/tidwall/sjson v1.2.5 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.71.0 // indirect + github.com/valyala/fasthttp v1.71.0 github.com/valyala/fastjson v1.6.10 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect golang.org/x/arch v0.24.0 // indirect diff --git a/pkg/channels/telegram/telegram.go b/pkg/channels/telegram/telegram.go index 93af5b4..1fe0eaf 100644 --- a/pkg/channels/telegram/telegram.go +++ b/pkg/channels/telegram/telegram.go @@ -21,6 +21,7 @@ import ( ta "github.com/mymmrac/telego/telegoapi" th "github.com/mymmrac/telego/telegohandler" tu "github.com/mymmrac/telego/telegoutil" + "github.com/valyala/fasthttp" "github.com/xibodev/compa/v3/pkg/bus" "github.com/xibodev/compa/v3/pkg/channels" @@ -85,32 +86,48 @@ type telegramMessageParts struct { mediaPaths []string } +// errorFillingCaller gives every Telegram API response an Error. telego +// formats each response's Error for a debug log whatever the log level, and +// formatting a nil *Error makes fmt recover a nil dereference. On Windows hosts +// whose CPUs have AMX, recovering such a fault can corrupt the heap +// (https://go.dev/issue/81238), and the kernel crashes in a later GC. +type errorFillingCaller struct{ ta.Caller } + +func (c errorFillingCaller) Call(ctx context.Context, url string, data *ta.RequestData) (*ta.Response, error) { + resp, err := c.Caller.Call(ctx, url, data) + if resp != nil && resp.Error == nil { + resp.Error = &ta.Error{} + } + return resp, err +} func NewTelegramChannel( bc *config.Channel, telegramCfg *config.TelegramSettings, bus *bus.MessageBus, ) (*TelegramChannel, error) { channelName := bc.Name() - var opts []telego.BotOption + // telego's own client, unless a proxy is set. + var caller ta.Caller = ta.FastHTTPCaller{Client: &fasthttp.Client{}} if telegramCfg.Proxy != "" { proxyURL, parseErr := url.Parse(telegramCfg.Proxy) if parseErr != nil { return nil, fmt.Errorf("invalid proxy URL %q: %w", telegramCfg.Proxy, parseErr) } - opts = append(opts, telego.WithHTTPClient(&http.Client{ + caller = ta.HTTPCaller{Client: &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyURL(proxyURL), }, - })) + }} } else if os.Getenv("HTTP_PROXY") != "" || os.Getenv("HTTPS_PROXY") != "" { // Use environment proxy if configured - opts = append(opts, telego.WithHTTPClient(&http.Client{ + caller = ta.HTTPCaller{Client: &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, }, - })) + }} } + opts := []telego.BotOption{telego.WithAPICaller(errorFillingCaller{caller})} if baseURL := strings.TrimRight(strings.TrimSpace(telegramCfg.BaseURL), "/"); baseURL != "" { opts = append(opts, telego.WithAPIServer(baseURL)) diff --git a/pkg/channels/telegram/telegram_access_test.go b/pkg/channels/telegram/telegram_access_test.go index 69edcf4..4f290b4 100644 --- a/pkg/channels/telegram/telegram_access_test.go +++ b/pkg/channels/telegram/telegram_access_test.go @@ -18,7 +18,7 @@ func newAccessTestChannel(t *testing.T, allowFrom ...string) (*TelegramChannel, t.Helper() getFiles := 0 bot, err := telego.NewBot("123456:"+strings.Repeat("a", 35), - telego.WithAPICaller(fileCountingCaller{getMeCaller: getMeCaller{username: "testbot"}, getFiles: &getFiles}), + telego.WithAPICaller(errorFillingCaller{fileCountingCaller{getMeCaller: getMeCaller{username: "testbot"}, getFiles: &getFiles}}), telego.WithDiscardLogger(), ) if err != nil { diff --git a/pkg/channels/telegram/telegram_group_command_filter_test.go b/pkg/channels/telegram/telegram_group_command_filter_test.go index e4d25f6..cd9e8cd 100644 --- a/pkg/channels/telegram/telegram_group_command_filter_test.go +++ b/pkg/channels/telegram/telegram_group_command_filter_test.go @@ -32,7 +32,7 @@ func newTestTelegramBot(t *testing.T, username string) *telego.Bot { token := "123456:" + strings.Repeat("a", 35) bot, err := telego.NewBot(token, - telego.WithAPICaller(getMeCaller{username: username}), + telego.WithAPICaller(errorFillingCaller{getMeCaller{username: username}}), telego.WithDiscardLogger(), ) if err != nil { @@ -165,7 +165,7 @@ func TestHandleMessage_GroupIgnoredBeforeDownloadingMedia(t *testing.T) { ch, messageBus := newGroupMentionOnlyChannel(t, "testbot") getFiles := 0 bot, err := telego.NewBot("123456:"+strings.Repeat("a", 35), - telego.WithAPICaller(fileCountingCaller{getMeCaller: getMeCaller{username: "testbot"}, getFiles: &getFiles}), + telego.WithAPICaller(errorFillingCaller{fileCountingCaller{getMeCaller: getMeCaller{username: "testbot"}, getFiles: &getFiles}}), telego.WithDiscardLogger(), ) if err != nil { diff --git a/pkg/channels/telegram/telegram_test.go b/pkg/channels/telegram/telegram_test.go index 6f98d82..faab6b1 100644 --- a/pkg/channels/telegram/telegram_test.go +++ b/pkg/channels/telegram/telegram_test.go @@ -9,6 +9,7 @@ import ( "net/http" "net/http/httptest" "os" + "path" "path/filepath" "strconv" "strings" @@ -144,7 +145,7 @@ func newTestChannelWithConstructor( t.Helper() bot, err := telego.NewBot(testToken, - telego.WithAPICaller(caller), + telego.WithAPICaller(errorFillingCaller{caller}), telego.WithRequestConstructor(constructor), telego.WithDiscardLogger(), ) @@ -165,6 +166,36 @@ func newTestChannelWithConstructor( } } +// Every response has an Error, so telego's debug formatting of it never +// dereferences a nil *Error; a failed response keeps its own. +func TestErrorFillingCaller(t *testing.T) { + failure := &ta.Error{ErrorCode: 400, Description: "Bad Request"} + responses := map[string]*ta.Response{ + "getMe": {Ok: true}, + "sendMessage": {Error: failure}, + } + caller := errorFillingCaller{&stubCaller{ + callFn: func(ctx context.Context, url string, data *ta.RequestData) (*ta.Response, error) { + if resp, ok := responses[path.Base(url)]; ok { + return resp, nil + } + return nil, errors.New("connection refused") + }, + }} + + resp, err := caller.Call(context.Background(), "https://api.telegram.org/bot/getMe", nil) + require.NoError(t, err) + require.NotNil(t, resp.Error) + assert.True(t, resp.Ok) + + resp, err = caller.Call(context.Background(), "https://api.telegram.org/bot/sendMessage", nil) + require.NoError(t, err) + assert.Same(t, failure, resp.Error) + + resp, err = caller.Call(context.Background(), "https://api.telegram.org/bot/getUpdates", nil) + assert.Nil(t, resp) + assert.EqualError(t, err, "connection refused") +} func TestSendMedia_ImageFallbacksToDocumentOnInvalidDimensions(t *testing.T) { constructor := &multipartRecordingConstructor{} caller := &stubCaller{