Propagate deferred SecurityContext supplier#19427
Open
oldium wants to merge 2 commits into
Open
Conversation
Micrometer Context Propagation invokes ThreadLocalAccessor.getValue() when capturing a snapshot and when clearing thread-locals for a scope. SecurityContextHolderThreadLocalAccessor materializes the deferred SecurityContext in getValue(), so snapshot capture triggers session loading; when that load itself subscribes to a Mono (e.g. Lettuce connecting to Redis), propagation re-enters the accessor and recurses until StackOverflowError. Issue spring-projectsgh-18059 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Oldřich Jedlička <oldium.pro@gmail.com>
SecurityContextHolderThreadLocalAccessor now propagates the deferred Supplier<SecurityContext> instead of materializing the SecurityContext during Micrometer context snapshot capture. Materializing could load the session from a store whose client subscribes to a Mono, which re-entered the accessor through context propagation and recursed until StackOverflowError. The new SecurityContextHolderStrategy.peekDeferredContext() returns the stored Supplier without invoking it and without auto-creating or storing a context, so snapshot capture has no side effects. getValue() keeps the pre-existing behavior of treating an empty SecurityContext as absent, applied only when the context is already materialized (an uninvoked deferred Supplier is propagated uninspected). The default peekDeferredContext() implementation materializes the current context eagerly, preserving pre-7.1 behavior for custom strategies. setDeferredContext() no longer re-wraps an already-validated Supplier, so micrometer's save/restore cycle does not accumulate wrappers. Closes spring-projectsgh-18059 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Oldřich Jedlička <oldium.pro@gmail.com>
Author
|
Just a note, this is not AI generated slop, it is completely human driven and human reviewed. Claude acted as a skillful assistant implementing the ideas, this is actually the third version of the code. First sketch was done with Opus 4.7, then updated with Opus 4.8 (added more tests) and now finished with Fable 5 (added back empty context handling into the Accessor) - and I am finally satisfied with the solution. The tests in the first commit expose the reported issue - they fail before the fix from commit 2 is applied. |
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.
Fixes a
StackOverflowErrorthat occurs when Micrometer Context Propagation captures or clears thread-locals while a deferredSecurityContextis set. Closes gh-18059.Problem
SecurityContextHolderThreadLocalAccessor.getValue()calledSecurityContextHolder.getContext(), which materializes the deferredSecurityContext. Micrometer invokesgetValue()during snapshot capture and also when clearing thread-locals for a scope (DefaultContextSnapshot.clearThreadLocalsaves the previous value before clearing), so anyMono.subscribewith automatic context propagation enabled can materialize the context — even when noSecurityContextkey is present in the ReactorContext. When materialization itself subscribes to aMono— e.g.RedisSessionRepository.findByIdvia Lettuce connecting to Redis — propagation re-enters the accessor,SupplierDeferredSecurityContext.init()re-invokes the supplier (it caches only after the supplier returns), and the cycle recurses untilStackOverflowError.Solution
Propagate the deferred
Supplier<SecurityContext>itself instead of the materialized context:SecurityContextHolderStrategy.peekDeferredContext()returns the storedSupplierwithout invoking it and without auto-creating or storing a context, so snapshot capture has no side effects. The default implementation materializes the current context eagerly (captured by value), preserving the previous propagation behavior for custom strategies that do not override it.SecurityContextHolderThreadLocalAccessornow implementsThreadLocalAccessor<Supplier<SecurityContext>>:getValue()usespeekDeferredContext()andsetValue(...)usessetDeferredContext(...), so the supplier is invoked lazily on the consuming thread.getValue()keeps the pre-existing behavior of treating an emptySecurityContextas absent, applied only when the context is already materialized (marked internally via a package-privateConstantSupplier). An uninvoked deferredSupplieris propagated uninspected — inspecting it would reintroduce the recursion.setDeferredContext(...)no longer re-wraps an already-validatedSupplier, so micrometer's save/restore cycle does not accumulate wrappers.Commits
The first commit adds tests that reproduce the issue against the previous implementation — they fail without the fix (recursion is bounded in the tests so a regression fails assertions instead of crashing the JVM). The second commit contains the fix. Squash if the intermediate failing-tests commit is not desired.
Notes for review
Contextunder theorg.springframework.security.core.context.SecurityContextkey changes type fromSecurityContexttoSupplier<SecurityContext>. This is inherent to deferring materialization. The key and theSecurityContextHolderAPI are unchanged, and micrometer invokes the accessor through the erasedThreadLocalAccessorinterface. The reactive accessor uses a different key (SecurityContext.class) and is unaffected.nullwas propagated after materializing it; the consuming thread observes an empty context either way.@since 7.1.1matches the currentmainversion; happy to adjust if this should target a different release.🤖 Generated with Claude Code
https://claude.ai/code/session_01MydkAGSDAf44MJnYJJDm5i