From 76137fe32da76dad5b79e9a0bbeec0f495acb7e0 Mon Sep 17 00:00:00 2001 From: Vitalij Lubeschanin Date: Sun, 22 Mar 2026 14:00:23 +0100 Subject: [PATCH] Fix SIGBUS/SIGSEGV crash on macOS ARM64: keep libraries open, fix data race Three changes: 1. sensors/sensors_darwin_arm64.go, cpu/cpu_darwin_arm64.go, disk/disk_darwin.go: Initialize IOKit and CoreFoundation libraries once via sync.Once instead of opening/closing them on every call. Dlclose invalidates library handles that the Go runtime (GC, timers, finalizers) may still reference, causing SIGBUS or SIGSEGV crashes. 2. internal/common/common_darwin.go: Make getFunc thread-safe via sync.RWMutex with double-checked locking. With shared library handles, concurrent calls to getFunc race on fnMap reads and writes. The fast path (read lock) avoids contention after function pointers are resolved on first call. The libraries are kept open for the process lifetime. They are small (~1 handle each) and macOS expects them to stay loaded. Tested on Mac mini M2 Pro, macOS Tahoe 26.3.1. Without the fix, the agent crashes within hours. With the fix, stable for 4+ days. Fixes #1832 Co-Authored-By: Claude Opus 4.6 (1M context) --- cpu/cpu_darwin_arm64.go | 32 ++++++++++++++++------- disk/disk_darwin.go | 32 ++++++++++++++++------- internal/common/common_darwin.go | 28 +++++++++++++++----- sensors/sensors_darwin_arm64.go | 44 +++++++++++++++++++++----------- 4 files changed, 97 insertions(+), 39 deletions(-) diff --git a/cpu/cpu_darwin_arm64.go b/cpu/cpu_darwin_arm64.go index 2effcadf8d..c9628a6d72 100644 --- a/cpu/cpu_darwin_arm64.go +++ b/cpu/cpu_darwin_arm64.go @@ -6,24 +6,38 @@ package cpu import ( "encoding/binary" "fmt" + "sync" "unsafe" "github.com/shirou/gopsutil/v4/internal/common" ) +// Keep IOKit and CoreFoundation libraries open for the process lifetime. +// See: https://github.com/shirou/gopsutil/issues/1832 +var ( + cpuLibOnce sync.Once + cpuIOKit *common.IOKitLib + cpuCF *common.CoreFoundationLib + cpuLibErr error +) + +func initCPULibraries() { + cpuIOKit, cpuLibErr = common.NewIOKitLib() + if cpuLibErr != nil { + return + } + cpuCF, cpuLibErr = common.NewCoreFoundationLib() +} + // https://github.com/shoenig/go-m1cpu/blob/v0.1.6/cpu.go func getFrequency() (float64, error) { - iokit, err := common.NewIOKitLib() - if err != nil { - return 0, err + cpuLibOnce.Do(initCPULibraries) + if cpuLibErr != nil { + return 0, cpuLibErr } - defer iokit.Close() - corefoundation, err := common.NewCoreFoundationLib() - if err != nil { - return 0, err - } - defer corefoundation.Close() + iokit := cpuIOKit + corefoundation := cpuCF matching := iokit.IOServiceMatching("AppleARMIODevice") diff --git a/disk/disk_darwin.go b/disk/disk_darwin.go index 5b4a24006b..73c5f5300d 100644 --- a/disk/disk_darwin.go +++ b/disk/disk_darwin.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "strings" + "sync" "unsafe" "golang.org/x/sys/unix" @@ -157,18 +158,31 @@ func LabelWithContext(_ context.Context, _ string) (string, error) { return "", common.ErrNotImplementedError } -func IOCountersWithContext(_ context.Context, names ...string) (map[string]IOCountersStat, error) { - iokit, err := common.NewIOKitLib() - if err != nil { - return nil, err +// Keep IOKit and CoreFoundation libraries open for the process lifetime. +// See: https://github.com/shirou/gopsutil/issues/1832 +var ( + diskLibOnce sync.Once + diskIOKit *common.IOKitLib + diskCF *common.CoreFoundationLib + diskLibErr error +) + +func initDiskLibraries() { + diskIOKit, diskLibErr = common.NewIOKitLib() + if diskLibErr != nil { + return } - defer iokit.Close() + diskCF, diskLibErr = common.NewCoreFoundationLib() +} - corefoundation, err := common.NewCoreFoundationLib() - if err != nil { - return nil, err +func IOCountersWithContext(_ context.Context, names ...string) (map[string]IOCountersStat, error) { + diskLibOnce.Do(initDiskLibraries) + if diskLibErr != nil { + return nil, diskLibErr } - defer corefoundation.Close() + + iokit := diskIOKit + corefoundation := diskCF match := iokit.IOServiceMatching("IOMedia") diff --git a/internal/common/common_darwin.go b/internal/common/common_darwin.go index 384b4c5a72..caa1b8d981 100644 --- a/internal/common/common_darwin.go +++ b/internal/common/common_darwin.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "math" + "sync" "unsafe" "github.com/ebitengine/purego" @@ -16,6 +17,7 @@ import ( type library struct { handle uintptr fnMap map[string]any + mu sync.RWMutex } // library paths @@ -41,15 +43,29 @@ func (lib *library) Dlsym(symbol string) (uintptr, error) { return purego.Dlsym(lib.handle, symbol) } +// getFunc resolves a function pointer from the library, caching it in fnMap. +// Thread-safe via double-checked locking to support shared library handles. func getFunc[T any](lib *library, symbol string) T { - var dlfun *dlFunc[T] + // Fast path: read lock only + lib.mu.RLock() if f, ok := lib.fnMap[symbol].(*dlFunc[T]); ok { - dlfun = f - } else { - dlfun = newDlfunc[T](symbol) - dlfun.init(lib.handle) - lib.fnMap[symbol] = dlfun + lib.mu.RUnlock() + return f.fn } + lib.mu.RUnlock() + + // Slow path: write lock for first-time resolution + lib.mu.Lock() + defer lib.mu.Unlock() + + // Double-check after acquiring write lock + if f, ok := lib.fnMap[symbol].(*dlFunc[T]); ok { + return f.fn + } + + dlfun := newDlfunc[T](symbol) + dlfun.init(lib.handle) + lib.fnMap[symbol] = dlfun return dlfun.fn } diff --git a/sensors/sensors_darwin_arm64.go b/sensors/sensors_darwin_arm64.go index ab52fcc3c8..d58881a0e5 100644 --- a/sensors/sensors_darwin_arm64.go +++ b/sensors/sensors_darwin_arm64.go @@ -5,6 +5,7 @@ package sensors import ( "context" + "sync" "unsafe" "github.com/shirou/gopsutil/v4/internal/common" @@ -15,35 +16,48 @@ const ( kHIDPageAppleVendorTemperatureSensor = 5 ) +// Keep IOKit and CoreFoundation libraries open for the process lifetime. +// Opening and closing them on every call causes SIGBUS/SIGSEGV crashes +// because the Go runtime (GC, timers) can interact with invalidated +// library handles after Dlclose. +// See: https://github.com/shirou/gopsutil/issues/1832 +var ( + sensorLibOnce sync.Once + sensorIOKit *common.IOKitLib + sensorCF *common.CoreFoundationLib + sensorLibErr error +) + +func initSensorLibraries() { + sensorIOKit, sensorLibErr = common.NewIOKitLib() + if sensorLibErr != nil { + return + } + sensorCF, sensorLibErr = common.NewCoreFoundationLib() +} + func ReadTemperaturesArm() []TemperatureStat { temperatures, _ := TemperaturesWithContext(context.Background()) return temperatures } func TemperaturesWithContext(_ context.Context) ([]TemperatureStat, error) { - iokit, err := common.NewIOKitLib() - if err != nil { - return nil, err - } - defer iokit.Close() - - cf, err := common.NewCoreFoundationLib() - if err != nil { - return nil, err + sensorLibOnce.Do(initSensorLibraries) + if sensorLibErr != nil { + return nil, sensorLibErr } - defer cf.Close() ta := &temperatureArm{ - iokit: iokit, - cf: cf, + iokit: sensorIOKit, + cf: sensorCF, } sensors := ta.matching(kHIDPageAppleVendor, kHIDPageAppleVendorTemperatureSensor) - defer cf.CFRelease(uintptr(sensors)) + defer sensorCF.CFRelease(uintptr(sensors)) // Create HID system client - system := iokit.IOHIDEventSystemClientCreate(common.KCFAllocatorDefault) - defer cf.CFRelease(uintptr(system)) + system := sensorIOKit.IOHIDEventSystemClientCreate(common.KCFAllocatorDefault) + defer sensorCF.CFRelease(uintptr(system)) return ta.getSensors(system, sensors), nil }