summaryrefslogtreecommitdiff
path: root/dir.c
diff options
context:
space:
mode:
authorJeremy Evans <code@jeremyevans.net>2023-01-16 13:29:43 -0800
committerJeremy Evans <code@jeremyevans.net>2023-03-24 11:18:57 -0700
commit466ca7ae205126c7cac83735db887d69e293f816 (patch)
tree2d2598ac52e1853f6afb8ddd0b1337616fd2647d /dir.c
parent5d6579bd9129cfbd62702fb42b249338807a34a2 (diff)
downloadruby-466ca7ae205126c7cac83735db887d69e293f816.tar.gz
Add Dir.fchdir
This is useful for passing directory file descriptors over UNIX sockets or to child processes to avoid TOCTOU vulnerabilities. The implementation follows the Dir.chdir code. This will raise NotImplementedError on platforms not supporting both fchdir and dirfd. Implements [Feature #19347]
Diffstat (limited to 'dir.c')
-rw-r--r--dir.c130
1 files changed, 130 insertions, 0 deletions
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);