From 466ca7ae205126c7cac83735db887d69e293f816 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Mon, 16 Jan 2023 13:29:43 -0800 Subject: 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] --- dir.c | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) (limited to 'dir.c') 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 + * fchdir instead of chdir 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 fchdir is the + * value of the block. fchdir and chdir blocks + * can be nested, but in a multi-threaded program an error will be raised + * if a thread attempts to open a fchdir or chdir + * block while another thread has one open or a call to fchdir + * or chdir without a block occurs inside a block passed to + * fchdir or chdir (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 + * + * produces: + * + * /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); -- cgit v1.2.1