Skip to content

Commit 5b078bb

Browse files
committed
Fix: Reload staging map races with apply/cancel
=== Classification Race condition in reload state handling. Severity: medium. Confidence: certain. === Affected Locations - `dnscrypt-proxy/plugin_forward.go:205` - `dnscrypt-proxy/plugin_forward.go:228` - `dnscrypt-proxy/plugin_forward.go:235` - `dnscrypt-proxy/plugin_forward.go:241` - `dnscrypt-proxy/plugin_forward.go:242` === Summary `PluginForward` stores staged reload data in `plugin.stagingMap` without consistently holding `rwLock`. `PrepareReload()` writes the staged map unlocked, `ApplyReload()` reads and nil-checks it before locking, and `CancelReload()` clears it unlocked. Concurrent reload sequences can therefore race on the shared slice header, causing spurious `no staged configuration to apply` failures or applying another reload's staged rules. === Provenance Verified from a reproduced finding derived from Swival Security Scanner output at https://swival.dev, then confirmed with a local concurrent reload reproducer and Go race detector evidence. === Preconditions Concurrent `PrepareReload`/`ApplyReload` or `PrepareReload`/`CancelReload` calls against the same `PluginForward` instance. === Proof A temporary concurrent test ran two `plugin.Reload()` loops against one `PluginForward`. `go test -race` reported concrete races on `plugin.stagingMap`, including: - write/write at `dnscrypt-proxy/plugin_forward.go:228` - write/read between `dnscrypt-proxy/plugin_forward.go:242` and `dnscrypt-proxy/plugin_forward.go:235` - write/read between `dnscrypt-proxy/plugin_forward.go:228` and `dnscrypt-proxy/plugin_forward.go:241` A non-race execution of the same reproducer also triggered real reload failures after sufficient iterations, including spurious `no staged configuration to apply` errors and cross-application of another reload's staged rules. === Why This Is A Real Bug This is not a detector-only issue. The raced value is the live reload handoff state that decides whether a staged configuration exists and which rules become active. Unsynchronized access to the slice header permits lost, stale, or mismatched staging state across overlapping reloads, directly producing incorrect runtime behavior during reachable reload paths such as file-watcher and SIGHUP-triggered reloads. === Fix Requirement Guard every read and write of `plugin.stagingMap`, including existence checks and nil assignment, with the same mutex used for reload state. === Patch Rationale The patch serializes all `stagingMap` access under `rwLock` in `PrepareReload()`, `ApplyReload()`, and `CancelReload()`. Moving the nil check inside the lock ensures `ApplyReload()` observes a consistent staged value, and locking the clear/write paths prevents concurrent reload operations from racing on the shared slice header. === Residual Risk None === Patch Patch file: `013-reload-staging-map-races-with-apply-cancel.patch`
1 parent 974b431 commit 5b078bb

File tree

1 file changed

+7
-3
lines changed

1 file changed

+7
-3
lines changed

dnscrypt-proxy/plugin_forward.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,30 +225,34 @@ func (plugin *PluginForward) PrepareReload() error {
225225
}
226226

227227
// Store in staging area
228+
plugin.rwLock.Lock()
228229
plugin.stagingMap = stagingMap
230+
plugin.rwLock.Unlock()
229231

230232
return nil
231233
}
232234

233235
// ApplyReload atomically replaces the active rules with the staging ones
234236
func (plugin *PluginForward) ApplyReload() error {
237+
plugin.rwLock.Lock()
238+
defer plugin.rwLock.Unlock()
239+
235240
if plugin.stagingMap == nil {
236241
return errors.New("no staged configuration to apply")
237242
}
238243

239-
// Use write lock to swap rule structures
240-
plugin.rwLock.Lock()
241244
plugin.forwardMap = plugin.stagingMap
242245
plugin.stagingMap = nil
243-
plugin.rwLock.Unlock()
244246

245247
dlog.Noticef("Applied new configuration for plugin [%s]", plugin.Name())
246248
return nil
247249
}
248250

249251
// CancelReload cleans up any staging resources
250252
func (plugin *PluginForward) CancelReload() {
253+
plugin.rwLock.Lock()
251254
plugin.stagingMap = nil
255+
plugin.rwLock.Unlock()
252256
}
253257

254258
// Reload implements hot-reloading for the plugin

0 commit comments

Comments
 (0)