Fix 2 — Writability fallback (lines 36-47)#69
Conversation
1aa9004 to
d2ec6e2
Compare
There was a problem hiding this comment.
Pull request overview
This PR adjusts Dir.tmpdir selection to better handle containerized environments where File.writable? (access(2)) can report a false negative, by adding a fallback acceptance path based on stat.writable?, and adds tests covering the new behavior.
Changes:
- Add a fallback in
Dir.tmpdirto accept a candidate directory whenFile.writable?is false butstat.writable?indicates it should be writable, emitting a warning. - Add tests for (1) the fallback acceptance-with-warning path and (2) the rejection path when both checks indicate non-writable.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| lib/tmpdir.rb | Adds the new “writability fallback” behavior and warning during tmpdir candidate selection. |
| test/test_tmpdir.rb | Adds regression tests to validate the fallback and non-writable rejection behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
971dcfa to
ee8cf4a
Compare
ee8cf4a to
cb04a6b
Compare
|
I tested this with a local Kubernetes cluster ( For default disk-backed {uid: 1000, euid: 1000, mode: "40777", writable: true, stat_writable: true, sticky: false, world: 511, write: true}So in the reproduced Kubernetes case:
Stock I also tested {tmpdir: "/tmp", mode: "41777", sticky: true}This suggests the Kubernetes/Rails issue is a sticky-bit problem, not a I also tested a read-only root filesystem without a writable File.writable?("/tmp") # false
File.stat("/tmp").writable? # true
File.write("/tmp/probe", "x") # Errno::EROFSWith this PR's fallback, Based on this, I think the correct fix is Kubernetes/pod configuration setting the sticky bit ( |
|
As a temporary application-level workaround until Kubernetes can mount disk-backed For example, in an app boot file that runs early: require "tmpdir"
class Dir
class << self
alias_method :tmpdir_without_kubernetes_emptydir_workaround, :tmpdir
def tmpdir
tmpdir_without_kubernetes_emptydir_workaround
rescue ArgumentError
dir = ENV["RUBY_TMPDIR_ALLOW_UNSAFE_EMPTYDIR"]
raise unless dir && !dir.empty?
dir = File.expand_path(dir)
stat = File.stat(dir)
raise unless stat.directory?
raise unless File.writable?(dir)
path = File.join(dir, ".ruby-tmpdir-check-#{Process.pid}")
File.open(path, File::WRONLY | File::CREAT | File::EXCL, 0600) {}
File.unlink(path)
warn "Using world-writable non-sticky tmpdir due to RUBY_TMPDIR_ALLOW_UNSAFE_EMPTYDIR=#{dir}: #{dir}"
dir
end
end
endThen configure the pod explicitly: env:
- name: TMPDIR
value: /tmp
- name: RUBY_TMPDIR_ALLOW_UNSAFE_EMPTYDIR
value: /tmpThis intentionally bypasses Ruby's sticky-bit safety check, so it should only be used in tightly controlled containers where the temp directory is not shared with untrusted users/processes. It also uses a real write probe, not Preferred fixes remain: mount a temp directory with mode |
In some containerized environments (Kubernetes with read-only root filesystem + emptyDir volumes), File.writable? (which uses the access(2) syscall) can return
false even though the directory is actually writable via mounted volumes.
See rails/rails#56997 for more context.
Changes
Added a fallback to stat.writable? when File.writable? fails.
Ruby 3.4 changed stat.writable? to File.writable? (which calls the kernel's access(2) syscall). In some container environments, access(2) can return false even though the directory IS writable (due to security modules, mounted volumes, etc.). We now fall back to stat.writable? — if the mode bits say writable, we accept the directory with a warning.
Usage for K8s users
This can occur in containerized environments with:
Kubernetes with read-only root filesystem + emptyDir volumes: The security context may restrict access checks at the syscall level, but the mounted volume is still writable SELinux/AppArmor policies: Security modules can affect access(2) results without actually blocking write operations NFS mounts with root squashing or specific export options: As you mentioned, NFS can exhibit similar behavior User namespaces in containers: UID mapping can cause access(2) to return incorrect results. The issue is that access(2) checks permissions based on the real UID/GID and may not account for all the kernel mechanisms that ultimately determine writability. The actual open()/write() syscalls can succeed even when access() says no.
I don't have a specific reproducible environment to share right now, but I can try to create a minimal Kubernetes manifest that demonstrates this if that would
help. @rhenium