Spring-security: AbstractUserDetailsReactiveAuthenticationManager should use Schedulers.boundidElastic()

Created on 11 Oct 2019  Â·  11Comments  Â·  Source: spring-projects/spring-security

Summary

The same Kotlin code (included) returns a 500 because block hound detects blocking code (FileInputStream.readBytes) when run against Oracle JDK 1.8.0_181. Only changing the runtime to OpenJDK 13, and no changes to the code, the same call is successful.

Configuration

Version

SpringBoot 2.2.RC1 (Spring Security 5.2)

Sample

https://github.com/MaxPowerDarrel/documents

backported waiting-for-triage

All 11 comments

Is there any updates on this issue?
I have same problem.

Java 11
Spring Security 5.2.1.RELEASE
Spring boot 2.2.1.RELEASE

The sample is gone. Can you provide a complete and minimal sample?

https://github.com/dlsrb6342/spring-security-blockhound

I pushed example.
Please check WithBlockHoundTest and WithoutBlockHoundTest.
With BlockHound, all tests are failed.

Following is error log

// WithBlockHoundTest.testController()
reactor.blockhound.BlockingOperationError: Blocking call! java.io.FileInputStream#readBytes
    at java.base/java.io.FileInputStream.readBytes(FileInputStream.java) ~[na:na]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ org.springframework.security.web.server.authentication.AuthenticationWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.web.server.context.ReactorContextWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.web.server.header.HttpHeaderWriterWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.config.web.server.ServerHttpSecurity$ServerWebExchangeReactorContextWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers$MutatorFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ HTTP GET "/controller" [ExceptionHandlingWebHandler]


// WithBlockHoundTest.testRouterFunction()
reactor.blockhound.BlockingOperationError: Blocking call! java.io.FileInputStream#readBytes
    at java.base/java.io.FileInputStream.readBytes(FileInputStream.java) ~[na:na]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ org.springframework.security.web.server.authentication.AuthenticationWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.web.server.context.ReactorContextWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.web.server.header.HttpHeaderWriterWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.config.web.server.ServerHttpSecurity$ServerWebExchangeReactorContextWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers$MutatorFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ HTTP GET "/routerfunction" [ExceptionHandlingWebHandler]

@rwinch Please have a look my example.

Thanks for the report. Spring Security AbstractUserDetailsReactiveAuthenticationManager uses publishOn Schedulers.newParallel before it encodes the password so I don't think this should happen. I can confirm that the Thread being used for encoding is password-encoder-* thread name.

reactor.blockhound.BlockingOperationError: Blocking call! java.io.FileInputStream#readBytes
    at java.base/java.io.FileInputStream.readBytes(FileInputStream.java) ~[na:na]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ org.springframework.security.web.server.authentication.AuthenticationWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.web.server.context.ReactorContextWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.web.server.header.HttpHeaderWriterWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.config.web.server.ServerHttpSecurity$ServerWebExchangeReactorContextWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers$MutatorFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ HTTP GET "/controller" [ExceptionHandlingWebHandler]
Stack trace:
        at java.base/java.io.FileInputStream.readBytes(FileInputStream.java) ~[na:na]
        at java.base/java.io.FileInputStream.read(FileInputStream.java:279) ~[na:na]
        at java.base/java.io.FilterInputStream.read(FilterInputStream.java:133) ~[na:na]
        at java.base/sun.security.provider.NativePRNG$RandomIO.readFully(NativePRNG.java:424) ~[na:na]
        at java.base/sun.security.provider.NativePRNG$RandomIO.ensureBufferValid(NativePRNG.java:526) ~[na:na]
        at java.base/sun.security.provider.NativePRNG$RandomIO.implNextBytes(NativePRNG.java:545) ~[na:na]
        at java.base/sun.security.provider.NativePRNG.engineNextBytes(NativePRNG.java:220) ~[na:na]
        at java.base/java.security.SecureRandom.nextBytes(SecureRandom.java:741) ~[na:na]
        at org.springframework.security.crypto.bcrypt.BCrypt.gensalt(BCrypt.java:832) ~[spring-security-core-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.security.crypto.bcrypt.BCrypt.gensalt(BCrypt.java:856) ~[spring-security-core-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder.encode(BCryptPasswordEncoder.java:106) ~[spring-security-core-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.security.crypto.password.DelegatingPasswordEncoder.encode(DelegatingPasswordEncoder.java:186) ~[spring-security-core-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager.lambda$authenticate$4(AbstractUserDetailsReactiveAuthenticationManager.java:109) ~[spring-security-core-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE]
        at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:67) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE]
        at reactor.core.publisher.FluxFilter$FilterSubscriber.onNext(FluxFilter.java:107) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE]
        at reactor.core.publisher.MonoPublishOn$PublishOnSubscriber.run(MonoPublishOn.java:178) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE]
        at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:68) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE]
        at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:28) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE]
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
        at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na]
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
        at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

A simplified test is

    @Test
    void blockhoundFailsWithParallelScheduler() {
        Scheduler passwordEncoderScheduler = Schedulers.newParallel("password-encoder", Schedulers.DEFAULT_POOL_SIZE, true);
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        Mono.delay(Duration.ofSeconds(1))
            .publishOn(passwordEncoderScheduler)
            .doOnNext(it -> {
                System.out.println(Thread.currentThread().getName());
                encoder.encode("hi");
            })
            .block();
    }

Perhaps @bsideup can help out here?

@rwinch Schedulers.newParallel will pass true to rejectBlocking of ReactorThreadFactory:
https://github.com/reactor/reactor-core/blob/a8022380f7d1eb9ef5bdcf0a9e0366d3dad534df/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java#L520-L524

Consider either using newParallel(int parallelism, ThreadFactory threadFactory) or newBoundedElastic(Schedulers.DEFAULT_POOL_SIZE, queueSizePerThread)

@bsideup Why does it do that? Is security actually broken? Do we need to change our behavior?

Because parallel schedulers are considered for non-blocking stuff (given the very low number of threads).

BTW just realized that there is a 3rd option:

Schedulers.fromExecutor(Executors.newFixedThreadPool(Schedulers.DEFAULT_POOL_SIZE))

After speaking with @bsideup offline, we should be using Schedulers.boundidElastic() since there are some JDKs which use blocking operations.

@rwinch
Thank you for quick response.
Can I know when this changes is released?

It will be released on June 3

FYI: You can click on any of the backported issues above and view the milestone and see when it is scheduled.

Was this page helpful?
0 / 5 - 0 ratings