Power Event Callback Utils - #1350
Conversation
|
Review complete. 1 potential issue to review. Files Reviewed: 3 By Severity:
Data race on unsynchronized callback globals in Windows power event handling — Register/Unregister write to handlers from Go goroutines while the callback reads them from a system thread with no synchronization, risking nil-panic and dropped events. Files Reviewed (3 files) |
There was a problem hiding this comment.
Review complete. 1 potential issue to review.
Summary
This PR adds cross-platform power suspend/resume notification stubs (ncutils/power_*.go) for the netclient WireGuard mesh VPN agent. Linux and Darwin are no-op stubs; Windows delivers events via RegisterSuspendResumeNotification.
Key Finding
finding-001 — High Severity: Data Race on Unsynchronized Callback Globals
File: ncutils/power_windows.go lines 35-96
Package-level globals onResumeAutomatic, onResumeSuspend (function pointers), and suspendResumeNotifyHandle (uintptr) are accessed concurrently without synchronization:
- Writers (Go goroutines):
RegisterPowerEventHandlersandUnregisterPowerEventHandlersmutate these variables. - Reader (Windows system thread):
powerEventCallback, invoked viasyscall.NewCallback, reads and calls them.
This creates a TOCTOU (time-of-check-time-of-use) vulnerability: if UnregisterPowerEventHandlers nulls a function pointer between the nil check and the call, a nil pointer dereference panic occurs on the system thread. Additionally, freshly registered handlers may be invisible to an already-dispatched callback due to lack of memory barriers.
Fix: Protect all accesses with a sync.RWMutex. The callback should acquire a read lock, snapshot the function pointers, release the lock, then invoke the snapshots.
Rejected Finding
A medium-severity edge case (finding-002, confidence 75) regarding misleading error messages when RegisterSuspendResumeNotification returns a NULL handle with GetLastError() == 0 was noted but fell below confidence threshold.
| func powerEventCallback(context, eventType, setting uintptr) uintptr { | ||
| switch eventType { | ||
| case PBT_APMRESUMEAUTOMATIC: | ||
| logger.Log(0, "windows power event: PBT_APMRESUMEAUTOMATIC (system resumed)") | ||
| if onResumeAutomatic != nil { | ||
| onResumeAutomatic() | ||
| } | ||
| case PBT_APMRESUMESUSPEND: | ||
| logger.Log(0, "windows power event: PBT_APMRESUMESUSPEND (user resumed interaction after suspend)") | ||
| if onResumeSuspend != nil { | ||
| onResumeSuspend() | ||
| } |
There was a problem hiding this comment.
🟠 Data race on unsynchronized callback globals (onResumeAutomatic, onResumeSuspend, suspendResumeNotifyHandle) (bug)
The package-level variables onResumeAutomatic, onResumeSuspend (function pointers declared at lines 38-39), and suspendResumeNotifyHandle (uintptr at line 35) are accessed concurrently with no synchronization.
Writers (Go goroutines):
RegisterPowerEventHandlerswritesonResumeAutomaticandonResumeSuspendat lines 50-51, andsuspendResumeNotifyHandleat line 68.UnregisterPowerEventHandlerswritessuspendResumeNotifyHandleat line 76-77, andonResumeAutomatic/onResumeSuspend(setting them to nil) at lines 79-80.
Reader (Windows system thread):
powerEventCallback(line 85) is invoked by Windows on a system thread viasyscall.NewCallback(line 54). It readsonResumeAutomaticat line 89 and calls it at line 90; readsonResumeSuspendat line 94 and calls it at line 95.
This is a data race under the Go memory model (Go 1.25 race detector would flag it). The critical danger is the TOCTOU (time-of-check-time-of-use) pattern: if onResumeAutomatic != nil { onResumeAutomatic() } — if UnregisterPowerEventHandlers nulls the function pointer between the nil check and the call, a nil function invocation (panic: nil pointer dereference) occurs. Conversely, a freshly registered handler may not be visible to an already-dispatched callback, causing events to be silently dropped.
💡 Suggestion: Protect all accesses to onResumeAutomatic, onResumeSuspend, and suspendResumeNotifyHandle with a sync.RWMutex. The callback (reader) acquires a read lock, snapshots the two function pointers into locals, releases the lock, then calls the non-nil snapshots. Register/Unregister (writers) acquire the write lock.
📋 Prompt for AI Agents
In ncutils/power_windows.go: (1) add var cbMu sync.RWMutex to the var block on line 30. (2) In RegisterPowerEventHandlers (line 47), hold cbMu.Lock() before the UnregisterPowerEventHandlers() call at line 48, and release it after suspendResumeNotifyHandle = handle at line 68. Also reorder: if the syscall fails at line 65-66, restore the handlers to nil before returning so the registration is atomic. (3) In UnregisterPowerEventHandlers (line 74), hold cbMu.Lock() around lines 75-80. (4) In powerEventCallback (line 85), acquire cbMu.RLock(), snapshot onResumeAutomatic and onResumeSuspend into local variables, release cbMu.RUnlock(), then check and call the local snapshots — this avoids holding the lock during user code execution on the system thread.
Describe your changes
Provide Issue ticket number if applicable/not in title
Provide link to Netmaker PR if required
Provide testing steps
Checklist before requesting a review