summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS.md6
-rw-r--r--configure.ac1
-rw-r--r--dir.c130
-rw-r--r--spec/ruby/core/dir/fchdir_spec.rb78
4 files changed, 215 insertions, 0 deletions
diff --git a/NEWS.md b/NEWS.md
index 53a7f2e723..c3118976bf 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -15,6 +15,11 @@ Note: We're only listing outstanding class updates.
* `Array#pack` now raises ArgumentError for unknown directives. [[Bug #19150]]
+* Dir
+
+ * `Dir.fchdir` added for changing the directory to the directory specified
+ by the provided directory file descriptor. [[Feature #19347]]
+
* String
* `String#unpack` now raises ArgumentError for unknown directives. [[Bug #19150]]
@@ -65,3 +70,4 @@ changelog for details of the default gems or bundled gems.
[Bug #19150]: https://bugs.ruby-lang.org/issues/19150
[Feature #19314]: https://bugs.ruby-lang.org/issues/19314
+[Feature #19347]: https://bugs.ruby-lang.org/issues/19347
diff --git a/configure.ac b/configure.ac
index 11c7b99f16..a0fadaa856 100644
--- a/configure.ac
+++ b/configure.ac
@@ -2018,6 +2018,7 @@ AC_CHECK_FUNCS(execv)
AC_CHECK_FUNCS(execve)
AC_CHECK_FUNCS(explicit_memset)
AC_CHECK_FUNCS(fcopyfile)
+AC_CHECK_FUNCS(fchdir)
AC_CHECK_FUNCS(fchmod)
AC_CHECK_FUNCS(fchown)
AC_CHECK_FUNCS(fcntl)
diff --git a/dir.c b/dir.c
index 20429e1516..c32e9de415 100644
--- a/dir.c
+++ b/dir.c
@@ -1094,6 +1094,135 @@ dir_s_chdir(int argc, VALUE *argv, VALUE obj)
return INT2FIX(0);
}
+#if defined(HAVE_FCHDIR) && defined(HAVE_DIRFD) && HAVE_FCHDIR && HAVE_DIRFD
+static void *
+nogvl_fchdir(void *ptr)
+{
+ const int *fd = ptr;
+
+ return (void *)(VALUE)fchdir(*fd);
+}
+
+static void
+dir_fchdir(int fd)
+{
+ if (fchdir(fd) < 0)
+ rb_sys_fail("fchdir");
+}
+
+struct fchdir_data {
+ VALUE old_dir;
+ int fd;
+ int done;
+};
+
+static VALUE
+fchdir_yield(VALUE v)
+{
+ struct fchdir_data *args = (void *)v;
+ dir_fchdir(args->fd);
+ args->done = TRUE;
+ chdir_blocking++;
+ if (NIL_P(chdir_thread))
+ chdir_thread = rb_thread_current();
+ return rb_yield_values(0);
+}
+
+static VALUE
+fchdir_restore(VALUE v)
+{
+ struct fchdir_data *args = (void *)v;
+ if (args->done) {
+ chdir_blocking--;
+ if (chdir_blocking == 0)
+ chdir_thread = Qnil;
+ dir_fchdir(RB_NUM2INT(dir_fileno(args->old_dir)));
+ }
+ dir_close(args->old_dir);
+ return Qnil;
+}
+
+/*
+ * call-seq:
+ * Dir.fchdir( integer ) -> 0
+ * Dir.fchdir( integer ) { block } -> anObject
+ *
+ * Changes the current working directory of the process to the directory
+ * specified by the given file descriptor integer. If the file descriptor
+ * is not valid, raises SystemCallError. One reason to use
+ * <code>fchdir</code> instead of <code>chdir</code> is when passing
+ * directory file descriptors over a UNIX socket or to child processes,
+ * to avoid TOCTOU (time-of-check to time-of-use) vulnerabilities.
+ *
+ * If a block is given, the current working directory is changed for the
+ * duration of the block, and the original working directory is restored
+ * when the block exits. The return value of <code>fchdir</code> is the
+ * value of the block. <code>fchdir</code> and <code>chdir</code> blocks
+ * can be nested, but in a multi-threaded program an error will be raised
+ * if a thread attempts to open a <code>fchdir</code> or <code>chdir</code>
+ * block while another thread has one open or a call to <code>fchdir</code>
+ * or <code>chdir</code> without a block occurs inside a block passed to
+ * <code>fchdir</code> or <code>chdir</code> (even in the same thread).
+ *
+ * When generating directory file descriptors from a +Dir+ instance,
+ * make sure the +Dir+ instance is not garbage collected before the
+ * directory file descriptor is passed to another process. Otherwise,
+ * the directory file descriptor will be closed before it is passed.
+ *
+ * dir = Dir.new("/var/spool/mail")
+ * dir2 = Dir.new("/usr")
+ * fd = dir.fileno
+ * fd2 = dir2.fileno
+ * Dir.fchdir(fd) do
+ * puts Dir.pwd
+ * Dir.fchdir(fd2) do
+ * puts Dir.pwd
+ * end
+ * puts Dir.pwd
+ * end
+ * puts Dir.pwd
+ *
+ * <em>produces:</em>
+ *
+ * /var/spool/mail
+ * /tmp
+ * /usr
+ * /tmp
+ * /var/spool/mail
+ */
+static VALUE
+dir_s_fchdir(VALUE klass, VALUE fd_value)
+{
+ int fd = RB_NUM2INT(fd_value);
+
+ if (chdir_blocking > 0) {
+ if (rb_thread_current() != chdir_thread)
+ rb_raise(rb_eRuntimeError, "conflicting chdir during another chdir block");
+ if (!rb_block_given_p())
+ rb_warn("conflicting chdir during another chdir block");
+ }
+
+ if (rb_block_given_p()) {
+ struct fchdir_data args;
+ args.old_dir = dir_s_alloc(klass);
+ dir_initialize(NULL, args.old_dir, rb_fstring_cstr("."), Qnil);
+ args.fd = fd;
+ args.done = FALSE;
+ return rb_ensure(fchdir_yield, (VALUE)&args, fchdir_restore, (VALUE)&args);
+ }
+ else {
+ int r = (int)(VALUE)rb_thread_call_without_gvl(nogvl_fchdir, &fd,
+ RUBY_UBF_IO, 0);
+ if (r < 0)
+ rb_sys_fail("fchdir");
+ }
+
+ return INT2FIX(0);
+}
+#else
+#define dir_s_fchdir rb_f_notimplement
+#endif
+
#ifndef _WIN32
VALUE
rb_dir_getwd_ospath(void)
@@ -3374,6 +3503,7 @@ Init_Dir(void)
rb_define_method(rb_cDir,"pos=", dir_set_pos, 1);
rb_define_method(rb_cDir,"close", dir_close, 0);
+ rb_define_singleton_method(rb_cDir,"fchdir", dir_s_fchdir, 1);
rb_define_singleton_method(rb_cDir,"chdir", dir_s_chdir, -1);
rb_define_singleton_method(rb_cDir,"getwd", dir_s_getwd, 0);
rb_define_singleton_method(rb_cDir,"pwd", dir_s_getwd, 0);
diff --git a/spec/ruby/core/dir/fchdir_spec.rb b/spec/ruby/core/dir/fchdir_spec.rb
new file mode 100644
index 0000000000..dde459e98e
--- /dev/null
+++ b/spec/ruby/core/dir/fchdir_spec.rb
@@ -0,0 +1,78 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+ruby_version_is '3.3' do
+ has_fchdir = begin
+ dir = Dir.new('.')
+ Dir.fchdir(dir.fileno)
+ true
+ rescue NotImplementedError
+ false
+ rescue Exception
+ true
+ ensure
+ dir.close
+ end
+
+ if has_fchdir
+ describe "Dir.fchdir" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ before :each do
+ @dirs = [Dir.new('.')]
+ @original = @dirs.first.fileno
+ end
+
+ after :each do
+ Dir.fchdir(@original)
+ @dirs.each(&:close)
+ end
+
+ it "changes to the specified directory" do
+ dir = Dir.new(DirSpecs.mock_dir)
+ @dirs << dir
+ Dir.fchdir dir.fileno
+ Dir.pwd.should == DirSpecs.mock_dir
+ end
+
+ it "returns 0 when successfully changing directory" do
+ Dir.fchdir(@original).should == 0
+ end
+
+ it "returns the value of the block when a block is given" do
+ Dir.fchdir(@original) { :block_value }.should == :block_value
+ end
+
+ it "changes to the specified directory for the duration of the block" do
+ pwd = Dir.pwd
+ dir = Dir.new(DirSpecs.mock_dir)
+ @dirs << dir
+ Dir.fchdir(dir.fileno) { Dir.pwd }.should == DirSpecs.mock_dir
+ Dir.pwd.should == pwd
+ end
+
+ it "raises a SystemCallError if the file descriptor given is not valid" do
+ -> { Dir.fchdir -1 }.should raise_error(SystemCallError)
+ -> { Dir.fchdir(-1) { } }.should raise_error(SystemCallError)
+ end
+
+ it "raises a SystemCallError if the file descriptor given is not for a directory" do
+ -> { Dir.fchdir $stdout.fileno }.should raise_error(SystemCallError)
+ -> { Dir.fchdir($stdout.fileno) { } }.should raise_error(SystemCallError)
+ end
+ end
+ else
+ describe "Dir.fchdir" do
+ it "raises NotImplementedError" do
+ -> { Dir.fchdir 1 }.should raise_error(NotImplementedError)
+ -> { Dir.fchdir(1) { } }.should raise_error(NotImplementedError)
+ end
+ end
+ end
+end