diff options
| -rw-r--r-- | NEWS.md | 6 | ||||
| -rw-r--r-- | configure.ac | 1 | ||||
| -rw-r--r-- | dir.c | 130 | ||||
| -rw-r--r-- | spec/ruby/core/dir/fchdir_spec.rb | 78 |
4 files changed, 215 insertions, 0 deletions
@@ -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) @@ -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 |
