Skip to content

Commit ae7d91a

Browse files
authored
Merge pull request #2119 from shirou/fix/darwin-errno-and-libcache
[darwin][process]: fix errno handling and library lifetime on darwin
2 parents b9930e2 + 49052a1 commit ae7d91a

9 files changed

Lines changed: 164 additions & 79 deletions

File tree

cpu/cpu_darwin.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ func perCPUTimes(sys *common.SystemLib) ([]TimesStat, error) {
143143
var cpuload *hostCpuLoadInfoData
144144

145145
status := sys.HostProcessorInfo(sys.MachHostSelf(), processorCpuLoadInfo,
146-
&ncpu, uintptr(unsafe.Pointer(&cpuload)), &count)
146+
&ncpu, unsafe.Pointer(&cpuload), &count)
147147

148148
if status != common.KERN_SUCCESS {
149149
return nil, fmt.Errorf("host_processor_info error=%d", status)
@@ -177,7 +177,7 @@ func allCPUTimes(sys *common.SystemLib) ([]TimesStat, error) {
177177
count := uint32(cpuStateMax)
178178

179179
status := sys.HostStatistics(sys.MachHostSelf(), common.HOST_CPU_LOAD_INFO,
180-
uintptr(unsafe.Pointer(&cpuload)), &count)
180+
unsafe.Pointer(&cpuload), &count)
181181

182182
if status != common.KERN_SUCCESS {
183183
return nil, fmt.Errorf("host_statistics error=%d", status)

disk/disk_darwin.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ func (i *ioCounters) fillStat(d uint32) (*IOCountersStat, error) {
327327
for key, off := range statstab {
328328
s := i.cfStr(key)
329329
if num := i.corefoundation.CFDictionaryGetValue(uintptr(v), uintptr(s)); num != nil {
330-
i.corefoundation.CFNumberGetValue(uintptr(num), common.KCFNumberSInt64Type, uintptr(unsafe.Add(unsafe.Pointer(&stat), off)))
330+
i.corefoundation.CFNumberGetValue(uintptr(num), common.KCFNumberSInt64Type, unsafe.Add(unsafe.Pointer(&stat), off))
331331
}
332332
i.corefoundation.CFRelease(uintptr(s))
333333
}

internal/common/common_darwin.go

Lines changed: 82 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,37 @@ const (
2727
SystemLibPath = "/usr/lib/libSystem.B.dylib"
2828
)
2929

30+
// Library handles are opened once and shared for the process lifetime.
31+
// Opening and closing them on every call causes SIGBUS/SIGSEGV crashes because
32+
// the Go runtime (GC, timers) can interact with invalidated library handles
33+
// after Dlclose. Sharing also keeps the resolved-symbol cache in fnMap warm,
34+
// so a symbol lookup is not repeated on every call.
35+
// See: https://github.com/shirou/gopsutil/issues/1832
36+
var (
37+
libCacheMu sync.Mutex
38+
libCache = make(map[string]*library)
39+
)
40+
3041
func newLibrary(path string) (*library, error) {
31-
lib, err := purego.Dlopen(path, purego.RTLD_LAZY|purego.RTLD_GLOBAL)
42+
libCacheMu.Lock()
43+
defer libCacheMu.Unlock()
44+
45+
if lib, ok := libCache[path]; ok {
46+
return lib, nil
47+
}
48+
49+
handle, err := purego.Dlopen(path, purego.RTLD_LAZY|purego.RTLD_GLOBAL)
3250
if err != nil {
3351
return nil, err
3452
}
3553

36-
return &library{
37-
handle: lib,
54+
lib := &library{
55+
handle: handle,
3856
fnMap: make(map[string]any),
39-
}, nil
57+
}
58+
libCache[path] = lib
59+
60+
return lib, nil
4061
}
4162

4263
func (lib *library) Dlsym(symbol string) (uintptr, error) {
@@ -69,9 +90,10 @@ func getFunc[T any](lib *library, symbol string) T {
6990
return dlfun.fn
7091
}
7192

72-
func (lib *library) Close() {
73-
purego.Dlclose(lib.handle)
74-
}
93+
// Close is a no-op, kept so that existing defer-Close call sites keep working.
94+
// Library handles are shared process-wide and are deliberately never unloaded;
95+
// see newLibrary for why.
96+
func (*library) Close() {}
7597

7698
type dlFunc[T any] struct {
7799
sym string
@@ -103,12 +125,12 @@ func (c *CoreFoundationLib) CFGetTypeID(cf uintptr) int64 {
103125
return fn(cf)
104126
}
105127

106-
func (c *CoreFoundationLib) CFNumberCreate(allocator uintptr, theType int64, valuePtr uintptr) unsafe.Pointer {
128+
func (c *CoreFoundationLib) CFNumberCreate(allocator uintptr, theType int64, valuePtr unsafe.Pointer) unsafe.Pointer {
107129
fn := getFunc[CFNumberCreateFunc](c.library, "CFNumberCreate")
108130
return fn(allocator, theType, valuePtr)
109131
}
110132

111-
func (c *CoreFoundationLib) CFNumberGetValue(num uintptr, theType int64, valuePtr uintptr) bool {
133+
func (c *CoreFoundationLib) CFNumberGetValue(num uintptr, theType int64, valuePtr unsafe.Pointer) bool {
112134
fn := getFunc[CFNumberGetValueFunc](c.library, "CFNumberGetValue")
113135
return fn(num, theType, valuePtr)
114136
}
@@ -247,7 +269,9 @@ func (l *IOKitLib) IOObjectRelease(object uint32) int32 {
247269
return fn(object)
248270
}
249271

250-
func (l *IOKitLib) IOConnectCallStructMethod(connection, selector uint32, inputStruct, inputStructCnt, outputStruct uintptr, outputStructCnt *uintptr) int32 {
272+
func (l *IOKitLib) IOConnectCallStructMethod(connection, selector uint32, inputStruct unsafe.Pointer, inputStructCnt uintptr,
273+
outputStruct unsafe.Pointer, outputStructCnt *uintptr,
274+
) int32 {
251275
fn := getFunc[IOConnectCallStructMethodFunc](l.library, "IOConnectCallStructMethod")
252276
return fn(connection, selector, inputStruct, inputStructCnt, outputStruct, outputStructCnt)
253277
}
@@ -294,14 +318,14 @@ func NewSystemLib() (*SystemLib, error) {
294318
return &SystemLib{library}, nil
295319
}
296320

297-
func (s *SystemLib) HostProcessorInfo(host uint32, flavor int32, outProcessorCount *uint32, outProcessorInfo uintptr,
321+
func (s *SystemLib) HostProcessorInfo(host uint32, flavor int32, outProcessorCount *uint32, outProcessorInfo unsafe.Pointer,
298322
outProcessorInfoCnt *uint32,
299323
) int32 {
300324
fn := getFunc[HostProcessorInfoFunc](s.library, "host_processor_info")
301325
return fn(host, flavor, outProcessorCount, outProcessorInfo, outProcessorInfoCnt)
302326
}
303327

304-
func (s *SystemLib) HostStatistics(host uint32, flavor int32, hostInfoOut uintptr, hostInfoOutCnt *uint32) int32 {
328+
func (s *SystemLib) HostStatistics(host uint32, flavor int32, hostInfoOut unsafe.Pointer, hostInfoOutCnt *uint32) int32 {
305329
fn := getFunc[HostStatisticsFunc](s.library, "host_statistics")
306330
return fn(host, flavor, hostInfoOut, hostInfoOutCnt)
307331
}
@@ -316,7 +340,7 @@ func (s *SystemLib) MachTaskSelf() uint32 {
316340
return fn()
317341
}
318342

319-
func (s *SystemLib) MachTimeBaseInfo(info uintptr) int32 {
343+
func (s *SystemLib) MachTimeBaseInfo(info unsafe.Pointer) int32 {
320344
fn := getFunc[MachTimeBaseInfoFunc](s.library, "mach_timebase_info")
321345
return fn(info)
322346
}
@@ -326,12 +350,12 @@ func (s *SystemLib) VMDeallocate(targetTask uint32, vmAddress, vmSize uintptr) i
326350
return fn(targetTask, vmAddress, vmSize)
327351
}
328352

329-
func (s *SystemLib) ProcPidPath(pid int32, buffer uintptr, bufferSize uint32) int32 {
353+
func (s *SystemLib) ProcPidPath(pid int32, buffer unsafe.Pointer, bufferSize uint32) int32 {
330354
fn := getFunc[ProcPidPathFunc](s.library, "proc_pidpath")
331355
return fn(pid, buffer, bufferSize)
332356
}
333357

334-
func (s *SystemLib) ProcPidInfo(pid, flavor int32, arg uint64, buffer uintptr, bufferSize int32) int32 {
358+
func (s *SystemLib) ProcPidInfo(pid, flavor int32, arg uint64, buffer unsafe.Pointer, bufferSize int32) int32 {
335359
fn := getFunc[ProcPidInfoFunc](s.library, "proc_pidinfo")
336360
return fn(pid, flavor, arg, buffer, bufferSize)
337361
}
@@ -341,16 +365,36 @@ func (s *SystemLib) ProcPidRusage(pid, flavor int32, buffer unsafe.Pointer) int3
341365
return fn(pid, flavor, buffer)
342366
}
343367

344-
func (s *SystemLib) Errno() int32 {
368+
// ErrnoLocation returns a pointer to the calling OS thread's errno, as given by
369+
// libc's __error(). The pointer is thread-local, so callers must hold
370+
// runtime.LockOSThread() across this call, the libc call being checked, and the
371+
// dereference of the returned pointer.
372+
//
373+
// Resolve the pointer *before* the libc call whose errno is to be inspected.
374+
// Resolving a symbol performs a dlsym and allocates, and either may overwrite
375+
// errno; doing it afterwards would race with the value being read.
376+
func (s *SystemLib) ErrnoLocation() *int32 {
345377
fn := getFunc[ErrnoFunc](s.library, "__error")
346-
return *fn()
378+
return fn()
347379
}
348380

349381
// status codes
350382
const (
351383
KERN_SUCCESS = 0
352384
)
353385

386+
// Arguments that point at Go memory are declared as unsafe.Pointer, a Go
387+
// pointer type, or a slice -- never as uintptr. purego maps uintptr to
388+
// uintptr_t, i.e. a plain integer: it neither keeps the pointee alive nor makes
389+
// it escape, so a Go local stays on the goroutine stack and the address goes
390+
// stale the moment the stack grows. Every such call then reads or writes the old
391+
// stack and silently sees or produces zeroes. The other kinds are passed through
392+
// reflect.Value.Pointer() and stay reachable for the duration of the call.
393+
//
394+
// uintptr remains correct for handles that are not Go memory: CoreFoundation and
395+
// IOKit object references, mach ports, addresses of dylib data symbols obtained
396+
// through Dlsym, and kernel-allocated vm addresses.
397+
354398
// IOKit types and constants.
355399
type (
356400
IOServiceGetMatchingServiceFunc func(mainPort uint32, matching uintptr) uint32
@@ -365,7 +409,8 @@ type (
365409
IORegistryEntryCreateCFPropertiesFunc func(entry uint32, properties unsafe.Pointer, allocator uintptr, options uint32) int32
366410
IOObjectConformsToFunc func(object uint32, className string) bool
367411
IOObjectReleaseFunc func(object uint32) int32
368-
IOConnectCallStructMethodFunc func(connection, selector uint32, inputStruct, inputStructCnt, outputStruct uintptr, outputStructCnt *uintptr) int32
412+
IOConnectCallStructMethodFunc func(connection, selector uint32, inputStruct unsafe.Pointer, inputStructCnt uintptr,
413+
outputStruct unsafe.Pointer, outputStructCnt *uintptr) int32
369414

370415
IOHIDEventSystemClientCreateFunc func(allocator uintptr) unsafe.Pointer
371416
IOHIDEventSystemClientSetMatchingFunc func(client, match uintptr) int32
@@ -390,10 +435,14 @@ const (
390435
)
391436

392437
// CoreFoundation types and constants.
438+
//
439+
// valuePtr on CFNumberCreate and CFNumberGetValue points at a caller-owned Go
440+
// value that CoreFoundation reads from or writes into, hence unsafe.Pointer; see
441+
// the note above the IOKit function types.
393442
type (
394443
CFGetTypeIDFunc func(cf uintptr) int64
395-
CFNumberCreateFunc func(allocator uintptr, theType int64, valuePtr uintptr) unsafe.Pointer
396-
CFNumberGetValueFunc func(num uintptr, theType int64, valuePtr uintptr) bool
444+
CFNumberCreateFunc func(allocator uintptr, theType int64, valuePtr unsafe.Pointer) unsafe.Pointer
445+
CFNumberGetValueFunc func(num uintptr, theType int64, valuePtr unsafe.Pointer) bool
397446
CFDictionaryCreateFunc func(allocator uintptr, keys, values *unsafe.Pointer, numValues int64,
398447
keyCallBacks, valueCallBacks uintptr) unsafe.Pointer
399448
CFDictionaryAddValueFunc func(theDict, key, value uintptr)
@@ -423,13 +472,16 @@ type MachTimeBaseInfo struct {
423472
Denom uint32
424473
}
425474

475+
// Buffers the kernel writes into are declared as unsafe.Pointer; see the note
476+
// above the IOKit function types. vmAddress on VMDeallocateFunc stays a uintptr
477+
// because it names kernel-allocated memory rather than Go memory.
426478
type (
427-
HostProcessorInfoFunc func(host uint32, flavor int32, outProcessorCount *uint32, outProcessorInfo uintptr,
479+
HostProcessorInfoFunc func(host uint32, flavor int32, outProcessorCount *uint32, outProcessorInfo unsafe.Pointer,
428480
outProcessorInfoCnt *uint32) int32
429-
HostStatisticsFunc func(host uint32, flavor int32, hostInfoOut uintptr, hostInfoOutCnt *uint32) int32
481+
HostStatisticsFunc func(host uint32, flavor int32, hostInfoOut unsafe.Pointer, hostInfoOutCnt *uint32) int32
430482
MachHostSelfFunc func() uint32
431483
MachTaskSelfFunc func() uint32
432-
MachTimeBaseInfoFunc func(info uintptr) int32
484+
MachTimeBaseInfoFunc func(info unsafe.Pointer) int32
433485
VMDeallocateFunc func(targetTask uint32, vmAddress, vmSize uintptr) int32
434486
)
435487

@@ -450,8 +502,8 @@ const (
450502
)
451503

452504
type (
453-
ProcPidPathFunc func(pid int32, buffer uintptr, bufferSize uint32) int32
454-
ProcPidInfoFunc func(pid, flavor int32, arg uint64, buffer uintptr, bufferSize int32) int32
505+
ProcPidPathFunc func(pid int32, buffer unsafe.Pointer, bufferSize uint32) int32
506+
ProcPidInfoFunc func(pid, flavor int32, arg uint64, buffer unsafe.Pointer, bufferSize int32) int32
455507
ProcPidRusageFunc func(pid, flavor int32, buffer unsafe.Pointer) int32
456508
ErrnoFunc func() *int32
457509
)
@@ -525,15 +577,18 @@ func NewSMC() (*SMC, error) {
525577
}, nil
526578
}
527579

528-
func (s *SMC) CallStruct(selector uint32, inputStruct, inputStructCnt, outputStruct uintptr, outputStructCnt *uintptr) int32 {
580+
func (s *SMC) CallStruct(selector uint32, inputStruct unsafe.Pointer, inputStructCnt uintptr,
581+
outputStruct unsafe.Pointer, outputStructCnt *uintptr,
582+
) int32 {
529583
return s.lib.IOConnectCallStructMethod(s.conn, selector, inputStruct, inputStructCnt, outputStruct, outputStructCnt)
530584
}
531585

586+
// Close releases the SMC connection. The IOKit handle itself is shared
587+
// process-wide and is deliberately left open; see newLibrary.
532588
func (s *SMC) Close() error {
533589
if result := s.lib.IOServiceClose(s.conn); result != 0 {
534590
return errors.New("ERROR: IOServiceClose failed")
535591
}
536-
s.lib.Close()
537592
return nil
538593
}
539594

@@ -555,10 +610,6 @@ func (s CStr) Ptr() *byte {
555610
return &s[0]
556611
}
557612

558-
func (s CStr) Addr() uintptr {
559-
return uintptr(unsafe.Pointer(s.Ptr()))
560-
}
561-
562613
func (s CStr) GoString() string {
563614
if s == nil {
564615
return ""

mem/mem_darwin.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func VirtualMemoryWithContext(_ context.Context) (*VirtualMemoryStat, error) {
9595
var vmstat vmStatisticsData
9696

9797
status := sys.HostStatistics(sys.MachHostSelf(), common.HOST_VM_INFO,
98-
uintptr(unsafe.Pointer(&vmstat)), &count)
98+
unsafe.Pointer(&vmstat), &count)
9999

100100
if status != common.KERN_SUCCESS {
101101
return nil, fmt.Errorf("host_statistics error=%d", status)

process/process.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,11 @@ type IOCountersStat struct {
109109
ReadCount uint64 `json:"readCount"`
110110
// WriteCount is a number of read I/O operations such as syscalls.
111111
WriteCount uint64 `json:"writeCount"`
112-
// ReadBytes is a number of all I/O read in bytes.
113-
// On Darwin, only disk I/O bytes are available.
112+
// ReadBytes is a number of all I/O read in bytes. This includes disk I/O on Linux and Windows.
113+
// Darwin does not expose this and reports 0; use DiskReadBytes there.
114114
ReadBytes uint64 `json:"readBytes"`
115-
// WriteBytes is a number of all I/O written in bytes.
116-
// On Darwin, only disk I/O bytes are available.
115+
// WriteBytes is a number of all I/O written in bytes. This includes disk I/O on Linux and Windows.
116+
// Darwin does not expose this and reports 0; use DiskWriteBytes there.
117117
WriteBytes uint64 `json:"writeBytes"`
118118
// DiskReadBytes is a number of disk I/O read in bytes. Currently, Linux and Darwin have this value.
119119
DiskReadBytes uint64 `json:"diskReadBytes"`

0 commit comments

Comments
 (0)