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.