From db6ca74e0be8a5f53a6f0a613fe29f463c66799c Mon Sep 17 00:00:00 2001 From: "nicolas.vandenbogaerde@gmail.com" Date: Tue, 17 Mar 2026 21:25:58 +0100 Subject: [PATCH 1/2] =?UTF-8?q?Fix=202=20=E2=80=94=20Writability=20fallbac?= =?UTF-8?q?k=20(lines=2036-47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/tmpdir.rb | 8 +++++++ test/test_tmpdir.rb | 52 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/lib/tmpdir.rb b/lib/tmpdir.rb index f78fd72..e5cafed 100644 --- a/lib/tmpdir.rb +++ b/lib/tmpdir.rb @@ -35,6 +35,14 @@ def self.tmpdir when !File.writable?(dir) # We call File.writable?, not stat.writable?, because you can't tell if a dir is actually # writable just from stat; OS mechanisms other than user/group/world bits can affect this. + # + # However, in some container environments (e.g. Kubernetes with read-only root filesystem + # and emptyDir volumes), File.writable? may return false even though the directory is + # actually writable via mounted volumes. Fall back to stat.writable? in that case. + if stat.writable? + warn "#{name}: File.writable? reports not writable but file mode bits suggest writable, using anyway: #{dir}" + break dir + end warn "#{name} is not writable: #{dir}" when stat.world_writable? && !stat.sticky? warn "#{name} is world-writable: #{dir}" diff --git a/test/test_tmpdir.rb b/test/test_tmpdir.rb index c91fc33..06830e0 100644 --- a/test/test_tmpdir.rb +++ b/test/test_tmpdir.rb @@ -59,6 +59,58 @@ def test_tmpdir_not_empty_parent end end + def test_writable_fallback_to_stat + omit "no meaning on this platform" if /mswin|mingw/ =~ RUBY_PLATFORM + Dir.mktmpdir do |tmpdir| + envs = %w[TMPDIR TMP TEMP] + oldenv = envs.each_with_object({}) {|v, h| h[v] = ENV.delete(v)} + begin + ENV[envs[0]] = tmpdir + + # Stub File.writable? to return false for our tmpdir + # This simulates container environments where access(2) returns false + # even though the directory is actually writable + original_writable = File.method(:writable?) + File.define_singleton_method(:writable?) do |path| + if path == tmpdir + false + else + original_writable.call(path) + end + end + + # Should fall back to stat.writable? and succeed with a warning + assert_equal(tmpdir, assert_warn(/File\.writable\? reports not writable but file mode bits suggest writable/) { Dir.tmpdir }) + + ensure + # Restore original File.writable? + File.define_singleton_method(:writable?, original_writable) if original_writable + ENV.update(oldenv) + end + end + end + + def test_writable_fallback_both_fail + omit "no meaning on this platform" if /mswin|mingw/ =~ RUBY_PLATFORM + Dir.mktmpdir do |tmpdir| + envs = %w[TMPDIR TMP TEMP] + oldenv = envs.each_with_object({}) {|v, h| h[v] = ENV.delete(v)} + begin + ENV[envs[0]] = tmpdir + + # Make directory not writable (both File.writable? and stat.writable? will return false) + File.chmod(0555, tmpdir) + + # Should reject the directory with "not writable" warning + assert_not_equal(tmpdir, assert_warn(/is not writable/) { Dir.tmpdir }) + + ensure + File.chmod(0755, tmpdir) + ENV.update(oldenv) + end + end + end + def test_no_homedir bug7547 = '[ruby-core:50793]' home, ENV["HOME"] = ENV["HOME"], nil From 1aa90041b0d799dab4f705b2c832aeee4f6a83b4 Mon Sep 17 00:00:00 2001 From: "nicolas.vandenbogaerde@gmail.com" Date: Tue, 17 Mar 2026 22:05:41 +0100 Subject: [PATCH 2/2] update --- test/test_tmpdir.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_tmpdir.rb b/test/test_tmpdir.rb index 06830e0..357fbad 100644 --- a/test/test_tmpdir.rb +++ b/test/test_tmpdir.rb @@ -92,6 +92,7 @@ def test_writable_fallback_to_stat def test_writable_fallback_both_fail omit "no meaning on this platform" if /mswin|mingw/ =~ RUBY_PLATFORM + omit "root can write to any directory" if Process.uid == 0 Dir.mktmpdir do |tmpdir| envs = %w[TMPDIR TMP TEMP] oldenv = envs.each_with_object({}) {|v, h| h[v] = ENV.delete(v)}