Guard sensitive_parameter_sinks reads with @registry_mutex - #81
Merged
Merged
Conversation
register_sensitive_parameter iterated the process-global sensitive_parameter_sinks Array without holding @registry_mutex, while on_sensitive_parameter appends to that same Array under the mutex. On MRI the GVL masks most damage, but on JRuby/TruffleRuby a thread class-loading a sensitive: true contract can race a thread installing a sink via an unguarded concurrent Array mutation-during-iteration, which can raise or silently drop a sink call. Snapshot the sinks under @registry_mutex before iterating, mirroring the replay in on_sensitive_parameter which already avoids holding the lock while invoking arbitrary sink callbacks. register_sensitive_parameter is never called while @registry_mutex is already held by the calling thread, so this introduces no deadlock risk. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
register_sensitive_parameter(lib/permittable.rb) iterated the process-globalsensitive_parameter_sinksArray without holding@registry_mutex:on_sensitive_parameter, meanwhile, appends to that very same Array under the mutex:sensitive_parameter_sinksis a plain lazily-initialized Array (@sensitive_parameter_sinks ||= []). On MRI the GVL masks most damage from an unsynchronized reader racing a synchronized writer, but on JRuby/TruffleRuby — real Rails deployment targets — this is a genuine data race: a thread class-loading asensitive: truecontract (which callsregister_sensitive_parameter's unguarded.each) can run concurrently with another thread callingPermittable.on_sensitive_parameter(mutex-guarded<<). An unguarded concurrent Array mutation-during-iteration can raise or silently drop a sink call — meaning a sensitive parameter name never reachesconfig.filter_parameters, a silent security regression.The fix
Take a snapshot of
sensitive_parameter_sinksunder@registry_mutexbefore iterating, then call the sinks against that snapshot outside the lock:This mirrors the pattern
on_sensitive_parameteralready uses for its own replay of previously-registered names (registry.names.each { ... }runs outside the mutex, after the guarded append) — so sink callbacks, which are arbitrary caller-supplied code, are never invoked while holding the lock, which would risk deadlock if a sink called back into a mutex-guarded Permittable method.Deadlock check:
register_sensitive_parameteris only ever called fromregister_sensitive_params(plural), which processes contract field metadata outside of any@registry_mutex.synchronizeblock — so taking the lock here cannot self-deadlock.Verification
spec/permittable_spec.rb), confirmed to fail before the implementation change and pass after. Since a real cross-thread race is inherently timing-dependent (and thus flaky to assert directly), the test instead asserts the invariant that rules the race out: every read ofsensitive_parameter_sinksfrom withinregister_sensitive_parameterhappens while@registry_mutexis locked, exactly like the append does.bundle exec rspec— 852 examples, 0 failuresbundle exec rubocop lib/permittable.rb spec/permittable_spec.rb— no offenses🤖 Generated with Claude Code