Skip to content

Avoid closing directory we're iterating #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

ekohl
Copy link
Contributor

@ekohl ekohl commented May 1, 2025

This starts with reverting #39 because it's broken and caused a regression. Then I've taken my commits from puppetlabs/puppet#9546.

Ruby 3.4 started error checking directory access and starts to raise Errno::EBADF.

This particular loop iterates on all open file descriptors and one is the directory listing from Dir.foreach.

In the past this could have led to leaked file descriptors, but it's unlikely since it's likely the last opened file descriptor and have the highest number.

Link: ruby/ruby@f2919bd
Link: https://bugzilla.redhat.com/show_bug.cgi?id=2349352

This is broken and results in not closing any file descriptors.
Comment on lines +482 to +484
ignore_fds = ['.', '..', d.fileno.to_s]
d.each_child do |f|
next if ignore_fds.include?(f) || f.to_i < 3
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at #41 I wonder if we should explicitly exclude the FDs that we want to keep open:

Suggested change
ignore_fds = ['.', '..', d.fileno.to_s]
d.each_child do |f|
next if ignore_fds.include?(f) || f.to_i < 3
ignore_fds = ['.', '..', stdin.fileno.to_s, stdout.fileno.to_s, stderr.fileno.to_s, d.fileno.to_s]
d.each_child do |f|
next if ignore_fds.include?(f)

This version is probably safer. Not sure if it's valid to have no stdin, stdout or stderr.

Suggested change
ignore_fds = ['.', '..', d.fileno.to_s]
d.each_child do |f|
next if ignore_fds.include?(f) || f.to_i < 3
ignore_fds = ['.', '..'] + [stdin, stdout, stderr, d].filter_map { |f| f&.fileno&.to_s }
d.each_child do |f|
next if ignore_fds.include?(f)

What makes me hesitant is that I don't know

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little worried we're playing whack-a-mole with things we want to filter out and I'm not sure how to be sure we're being exhaustive about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO this is much better than looking at .to_i < 3 and assuming that those file descriptors are stdin, stdout and stderr since it is explicit about what's going on.

@ekohl
Copy link
Contributor Author

ekohl commented May 1, 2025

And I just noticed this repo doesn't run any of the tests, but when I locally run it I did break them:

describe "safe_posix_fork", unless: Puppet::Util::Platform.windows? || Puppet::Util::Platform.jruby? do
let(:pid) { 5501 }
before :each do
# Most of the things this method does are bad to do during specs. :/
allow(Kernel).to receive(:fork).and_return(pid).and_yield
allow($stdin).to receive(:reopen)
allow($stdout).to receive(:reopen)
allow($stderr).to receive(:reopen)
# ensure that we don't really close anything!
allow(IO).to receive(:new)
end
it "should close all open file descriptors except stdin/stdout/stderr when /proc/self/fd exists" do
# This is ugly, but I can't really think of a better way to do it without
# letting it actually close fds, which seems risky
fds = [".", "..","0","1","2","3","5","100","1000"]
fds.each do |fd|
if fd == '.' || fd == '..'
next
elsif ['0', '1', '2'].include? fd
expect(IO).not_to receive(:new).with(fd.to_i)
else
expect(IO).to receive(:new).with(fd.to_i).and_return(double('io', close: nil))
end
end
dir_expectation = receive(:foreach).with('/proc/self/fd')
fds.each do |fd|
dir_expectation = dir_expectation.and_yield(fd)
end
allow(Dir).to dir_expectation
Puppet::Util.safe_posix_fork
end
it "should close all open file descriptors except stdin/stdout/stderr when /proc/self/fd doesn't exist" do
# This is ugly, but I can't really think of a better way to do it without
# letting it actually close fds, which seems risky
(0..2).each {|n| expect(IO).not_to receive(:new).with(n)}
(3..256).each {|n| expect(IO).to receive(:new).with(n).and_return(double('io', close: nil)) }
allow(Dir).to receive(:foreach).with('/proc/self/fd').and_raise(Errno::ENOENT)
Puppet::Util.safe_posix_fork
end
it "should close all open file descriptors except stdin/stdout/stderr when /proc/self is not a directory" do
# This is ugly, but I can't really think of a better way to do it without
# letting it actually close fds, which seems risky
(0..2).each {|n| expect(IO).not_to receive(:new).with(n)}
(3..256).each {|n| expect(IO).to receive(:new).with(n).and_return(double('io', close: nil)) }
allow(Dir).to receive(:foreach).with('/proc/self/fd').and_raise(Errno::ENOTDIR)
Puppet::Util.safe_posix_fork
end
it "should fork a child process to execute the block" do
expect(Kernel).to receive(:fork).and_return(pid).and_yield
Puppet::Util.safe_posix_fork do
"Fork this!"
end
end
it "should return the pid of the child process" do
expect(Puppet::Util.safe_posix_fork).to eq(pid)
end
end

ekohl added 2 commits May 1, 2025 13:10
Ruby 3.4 started error checking directory access and starts to raise
Errno::EBADF.

This particular loop iterates on all open file descriptors and one is
the directory listing from Dir.foreach.

In the past this could have led to leaked file descriptors, but it's
unlikely since it's likely the last opened file descriptor and have the
highest number.

Link: ruby/ruby@f2919bd
Link: https://bugzilla.redhat.com/show_bug.cgi?id=2349352
@ekohl ekohl force-pushed the really-fix-file-closing branch from 5748bab to 3f344ab Compare May 1, 2025 11:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feature request]: Fix file descriptor management in safe_posix_fork
2 participants