From: "ioquatix (Samuel Williams)" Date: 2022-09-20T22:01:46+00:00 Subject: [ruby-core:109965] [Ruby master Bug#18782] Race conditions in autoload when loading the same feature with multiple threads. Issue #18782 has been updated by ioquatix (Samuel Williams). Status changed from Open to Closed Assignee set to ioquatix (Samuel Williams) I've confirmed after my PRs, this issue cannot be reproduced on HEAD. ---------------------------------------- Bug #18782: Race conditions in autoload when loading the same feature with multiple threads. https://bugs.ruby-lang.org/issues/18782#change-99215 * Author: ioquatix (Samuel Williams) * Status: Closed * Priority: Normal * Assignee: ioquatix (Samuel Williams) * Backport: 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN ---------------------------------------- I have identified several race conditions in the autoload code. 1. It's possible to race on adding and then deleting items in `autoload_featuremap`. When this happens, two threads will try to load the same file with different autoload data and deadlock. 2. When finishing autoload, it's necessary to clear `ele->state` before setting constants. If this is not synchronised, a thread can see the cleared `ele->state` before seeing the constants and assume the constant is not being autoloaded and then fail with `NameError`. This test case can reproduce both cases: ``` # test.rb autoload_path = File.join(__dir__, "foobar.rb") File.write(autoload_path, 'module Foo; end; module Bar; end') 100_000.times do $stderr.puts "--------------------" autoload :Foo, autoload_path autoload :Bar, autoload_path t1 = Thread.new {Foo} t2 = Thread.new {Bar} t1.join t2.join Object.send(:remove_const, :Foo) Object.send(:remove_const, :Bar) $LOADED_FEATURES.delete(autoload_path) end ``` Example failure of case (1): ``` -------------------- (success) autoload_by_someone_else ele=0x55f33b806a30 ele->state=(nil) autoload_by_someone_else ele=0x55f33b806a30 ele->state=(nil) check_autoload_required 2 autoload_by_someone_else ele=0x55f33b806a30 ele->state=0x7fdd678be780 check_autoload_required 4 autoload_by_someone_else ele=0x55f33b806a30 ele->state=0x7fdd678be780 check_autoload_required 4 ele=0x55f33b806a30 ele->state=0x7fdd678be780 = NULL check_autoload_required 4 -------------------- (failure) autoload_by_someone_else ele=0x55f33b806a30 ele->state=(nil) autoload_by_someone_else ele=0x55f33b6e8f40 ele->state=(nil) check_autoload_required 2 check_autoload_required 3 autoload_by_someone_else ele=0x55f33b806a30 ele->state=0x7fdd6779d780 check_autoload_required 1 autoload_by_someone_else ele=0x55f33b806a30 ele->state=0x7fdd6779d780 check_autoload_required 1 ele=0x55f33b806a30 ele->state=0x7fdd6779d780 = NULL ele=0x55f33b6e8f40 ele->state=0x7fdd678be780 = NULL ../test.rb:12:in `join': No live threads left. Deadlock? (fatal) 3 threads, 3 sleeps current:0x000055f33b771250 main thread:0x000055f33b66e090 * # rb_thread_t:0x000055f33b66e090 native:0x00007fdd6a71c3c0 int:0 * # rb_thread_t:0x000055f33b770ff0 native:0x00007fdd6789e640 int:1 mutex:0x000055f33b7c5100 cond:1 depended by: tb_thread_id:0x000055f33b66e090 * # rb_thread_t:0x000055f33b771250 native:0x00007fdd679bf640 int:0 from ../test.rb:12:in `block in
' from ../test.rb:4:in `times' from ../test.rb:4:in `
' make: *** [uncommon.mk:1250: runruby] Error 1 ``` Example failure of case (2): ``` [0x7f175fe5b0c8] rb_autoload_str mod=Object id=Foo file="/home/samuel/Projects/ioquatix/ruby/foobar.rb" [0x7f175fe5b0c8] rb_autoload_str const_set mod=Object id=Foo file="/home/samuel/Projects/ioquatix/ruby/foobar.rb" [0x7f175fe5b0c8] rb_autoload_str mod=Object id=Bar file="/home/samuel/Projects/ioquatix/ruby/foobar.rb" [0x7f175fe5b0c8] rb_autoload_str const_set mod=Object id=Bar file="/home/samuel/Projects/ioquatix/ruby/foobar.rb" [0x7f175fe61d88] rb_const_search_from value == Qundef -> autoloading [0x7f175fe61e78] rb_const_search_from value == Qundef -> autoloading [0x7f175fe61e78] Assigning constants... [0x7f175fe61d88] rb_const_search_from value == Qundef -> autoloading [0x7f175fe61e78] autoload_const_set name=:Foo value=Foo [0x7f175fe61e78] autoload_const_set name=:Bar value=Bar # terminated with exception (report_on_exception is true): ../test.rb:11:in `block (2 levels) in
': uninitialized constant Bar (NameError) ../test.rb:11:in `block (2 levels) in
': uninitialized constant Bar (NameError) make: *** [uncommon.mk:1250: runruby] Error 1 ``` These failures are very uncommon but it does impact Ruby as far back as 2.7, and probably earlier. ---Files-------------------------------- 0001-Add-RUBY_VM_CRITICAL_SECTION-for-detecting-unexpecte.patch (2.07 KB) 0002-Prevent-race-between-GC-mark-and-autoload-setup.patch (1.01 KB) 0003-Protect-race-on-autoload-state.patch (10.6 KB) 0004-Increase-timeout-for-debug-tests.patch (910 Bytes) 0005-Revert-removal-of-non-conditional-xfree.patch (691 Bytes) -- https://bugs.ruby-lang.org/ Unsubscribe: