Visitar URL original
With invokedynamic, class_attribute writes can leak into the superclass after define_method on the subclass's singleton class · Issue #9777 · jruby/jruby · GitHub
Skip to content

With invokedynamic, class_attribute writes can leak into the superclass after define_method on the subclass's singleton class #9777

Description

@gillesbergerp

Problem

ActiveSupport's class_attribute keeps each class's value in closures behind methods on the singleton class (ClassAttribute.redefine). A subclass reads through its parent's singleton methods until its first write. That write calls define_method on the subclass's singleton class to give it its own reader and writer.

ActiveSupport::Callbacks::ClassMethods#set_callbacks writes and then reads in the same call:

self.__callbacks = __callbacks.dup             # L948: first write defines the subclass's own reader/writer
callbacks_was = self.__callbacks[name.to_sym]  # L951
self.__callbacks[name.to_sym] = callbacks      # L955

With invokedynamic on, the reads on L951/L955 intermittently still return the parent's hash after L948 has defined the subclass's reader. The callback is stored in the parent, and the subclass's own copy doesn't get it.

We captured this during Rails eager loading, on the main thread, by wrapping set_callbacks. Right after the write, we read the hash three ways:

reader owner bind_call / send / public reader process_action chain
Child before L948 #<Class:ApplicationController> 4488 / 4488 / 4488 parent's 9
Child after set_callbacks #<Class:ChildController> 4612 / 4612 / 4612 parent's 9, missing the new callback
ApplicationController after #<Class:ApplicationController> 4488 / 4488 / 4488 parent's 9 + the child's callback

The child owns a new hash (4612), and freshly resolved calls return it. But inside set_callbacks, which is hot and compiled by then, self.__callbacks returned 4488. A hot __callbacks.equal?(superclass.__callbacks) check in the same frame returned true, while the same check in cold code a moment later returned false.

The child's singleton class had modules extended into it from included. Class has ReloadedClassesFiltering prepended, as Rails does.

Impact

A controller's callbacks leak into its parent and into every sibling that loads afterwards, usually silently. It becomes visible when a later sibling calls skip_before_action for a callback that an earlier sibling's leaked skip already removed from the parent:

ArgumentError: Before process_action callback :some_filter has not been defined
  activesupport-8.1.3.1/lib/active_support/callbacks.rb:798:in 'block in skip_callback'
  zeitwerk-2.7.5/lib/zeitwerk/loader/eager_load.rb:171:in 'block in actual_eager_load_dir'
  railties-8.1.3.1/lib/rails/application/finisher.rb:79:in 'block in Finisher'

Measurements

Same app, about 36 JRuby boots per CI run:

Setup Runs Runs with a leak
JRuby 10.0.7.0 5 4
JRuby 10.0.5.0 5 2
JRuby 10.0.7.0, -Xcompile.invokedynamic=false 6 0

Reproduction

No standalone reproduction yet. Outside the app, activesupport/actionpack 8.1.3.1 on JRuby 10.0.7.0 didn't leak in 1,000+ rounds of controller subclasses using before_action/skip_before_action, modules extended from included, active_support/all, and concurrent class definition. That held under -X-C, the default JIT, and -Xjit.threshold=0 -Xjit.background=false. In the app this initializer catches it:

module DiagnoseCallbacks
  protected

  def set_callbacks(name, callbacks)
    super.tap do
      parent = superclass
      if parent.respond_to?(:__callbacks) && __callbacks.equal?(parent.__callbacks)
        reader = singleton_class.instance_method(:__class_attr___callbacks)
        warn("#{inspect} wrote into #{parent.inspect}'s hash: owner #{reader.owner.inspect}, " \
             "bind_call #{reader.bind_call(self).object_id}, send #{send(:__class_attr___callbacks).object_id}, " \
             "public #{__callbacks.object_id}, parent #{parent.__callbacks.object_id}")
      end
    end
  end
end

ActiveSupport.on_load(:action_controller_base) { singleton_class.prepend(DiagnoseCallbacks) }

Expected: once define_method adds the reader to the subclass's singleton class, every call site dispatches to it, as send, bind_call and cold call sites do.

Workaround

-Xcompile.invokedynamic=false

Environment Information
JRuby 10.0.7.0 (also 10.0.5.0)
Amazon Corretto 26.0.2
Rails 8.1.3.1
Zeitwerk 2.7.5

Activity

  1. headius commented on Oct 7, 2026

    @headius
    Member

    Good catch.

  2. added this to the JRuby 10.0.8.0 milestone on Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions