Skip to content

Circular exception handling can cause infinite loop since 9.3.4.0 #7267

@rsim

Description

@rsim

Environment Information

  • jruby 9.3.6.0 (2.6.8) 2022-06-27 7a2cbcd376 OpenJDK 64-Bit Server VM 11.0.12+7 on 11.0.12+7 +jit [x86_64-linux]
  • Linux production-app1 4.4.0-210-generic #242-Ubuntu SMP Fri Apr 16 09:57:56 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

Expected Behavior

As a result of bd2595c which solved #7035 the checkCircularCause method was introduced. It's purpose is to detect circular loops of exception causes.

Actual Behavior

After upgrading our Rails app from JRuby 9.3.3.0 to 9.3.6.0 we discovered an infinite loop in the Java Finalizer thread. In each thread dump we see that it is stuck in the checkCircularCause method:

"Finalizer" - Thread t@3
   java.lang.Thread.State: RUNNABLE
        at org.jruby.RubyException.checkCircularCause(RubyException.java:399)
        at org.jruby.RubyException.setCause(RubyException.java:370)
        at org.jruby.Ruby.newRaiseException(Ruby.java:4299)
        at org.jruby.Ruby.newArgumentError(Ruby.java:3631)
        at org.jruby.Ruby.newArgumentError(Ruby.java:3626)
        at org.jruby.runtime.Arity.checkArity(Arity.java:167)
        at org.jruby.runtime.Arity.checkArity(Arity.java:161)
        at org.jruby.ext.ffi.jffi.NativeInvoker.call(NativeInvoker.java:70)
        at org.jruby.ext.ffi.jffi.DefaultMethod.call(DefaultMethod.java:106)
        at org.jruby.internal.runtime.methods.DynamicMethod.call(DynamicMethod.java:222)
        at org.jruby.RubyMethod.call(RubyMethod.java:119)
        at org.jruby.RubyMethod$INVOKER$i$call.call(RubyMethod$INVOKER$i$call.gen)
        at org.jruby.internal.runtime.methods.JavaMethod$JavaMethodZeroOrOneOrNBlock.call(JavaMethod.java:354)
        at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:173)
        at home.rails.eazybi.production.shared.bundle.jruby.$2_dot_6_dot_0.gems.ffi_minus_1_dot_15_dot_5_minus_java.lib.ffi.autopointer.invokeOther1:call(/home/rails/eazybi/production/shared/bundle/jruby/2.6.0/gems/ffi-1.15.5-java/lib/ffi/autopointer.rb:175)
        at home.rails.eazybi.production.shared.bundle.jruby.$2_dot_6_dot_0.gems.ffi_minus_1_dot_15_dot_5_minus_java.lib.ffi.autopointer.RUBY$method$release$0(/home/rails/eazybi/production/shared/bundle/jruby/2.6.0/gems/ffi-1.15.5-java/lib/ffi/autopointer.rb:175)
        at java.lang.invoke.LambdaForm$DMH/0x0000000801af7840.invokeStatic(LambdaForm$DMH)
        at java.lang.invoke.LambdaForm$MH/0x00000008006f2040.invokeExact_MT(LambdaForm$MH)
        at org.jruby.internal.runtime.methods.CompiledIRMethod.call(CompiledIRMethod.java:165)
        at org.jruby.internal.runtime.methods.MixedModeIRMethod.call(MixedModeIRMethod.java:185)
        at org.jruby.internal.runtime.methods.DynamicMethod.call(DynamicMethod.java:218)
        at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:173)
        at home.rails.eazybi.production.shared.bundle.jruby.$2_dot_6_dot_0.gems.ffi_minus_1_dot_15_dot_5_minus_java.lib.ffi.autopointer.invokeOther3:release(/home/rails/eazybi/production/shared/bundle/jruby/2.6.0/gems/ffi-1.15.5-java/lib/ffi/autopointer.rb:150)
        at home.rails.eazybi.production.shared.bundle.jruby.$2_dot_6_dot_0.gems.ffi_minus_1_dot_15_dot_5_minus_java.lib.ffi.autopointer.RUBY$method$call$0(/home/rails/eazybi/production/shared/bundle/jruby/2.6.0/gems/ffi-1.15.5-java/lib/ffi/autopointer.rb:150)
        at java.lang.invoke.LambdaForm$DMH/0x0000000801af7440.invokeStatic(LambdaForm$DMH)
        at java.lang.invoke.LambdaForm$MH/0x00000008006f2040.invokeExact_MT(LambdaForm$MH)
        at org.jruby.internal.runtime.methods.CompiledIRMethod.call(CompiledIRMethod.java:139)
        at org.jruby.internal.runtime.methods.CompiledIRMethod.call(CompiledIRMethod.java:162)
        at org.jruby.internal.runtime.methods.MixedModeIRMethod.call(MixedModeIRMethod.java:185)
        at org.jruby.internal.runtime.methods.DynamicMethod.call(DynamicMethod.java:218)
        at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:173)
        at org.jruby.RubyBasicObject$Finalizer.callFinalizer(RubyBasicObject.java:1971)
        at org.jruby.RubyBasicObject$Finalizer.finalize(RubyBasicObject.java:1960)
        at java.lang.System$2.invokeFinalize(System.java:2125)
        at java.lang.ref.Finalizer.runFinalizer(Finalizer.java:87)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:171)

It seems that it is caused by an invalid FFI::AutoPointer definition in the appsignal gem (I reported the issue to them appsignal/appsignal-ruby#854) which causes org.jruby.runtime.Arity.checkArity to fail and which raises an ArgumentError which then causes an infinite loop in checkCircularCause.

I was not able to reproduce this infinite loop with a simple script. But as I was looking at the source of this method

private void checkCircularCause(IRubyObject cause) {
    IRubyObject currentCause = cause;
    while (currentCause instanceof RubyException) {
        if (currentCause == this) {
            RaiseException runtimeError = getRuntime().newRuntimeError("circular causes");
            runtimeError.getException().setCause(cause);
            throw runtimeError;
        }
        currentCause = ((RubyException) currentCause).cause;
    }
}

I suspect that in this case there is a circular loop of causes but which does not include the current exception (this). I think that it would be better to add prevention of potential infinite loop by

  • adding a loop counter and exiting the loop if the counter is too large
  • or using a Set where all causes are added and raising an exception if any duplicate cause is found in this loop.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions