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

Comment From: dlsrb6342

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

Comment From: rwinch

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

Comment From: dlsrb6342

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]

Comment From: dlsrb6342

@rwinch Please have a look my example.

Comment From: rwinch

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?

Comment From: bsideup

@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)

Comment From: rwinch

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

Comment From: bsideup

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))

Comment From: rwinch

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

Comment From: dlsrb6342

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

Comment From: rwinch

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.