diff options
author | Jeremy Evans <code@jeremyevans.net> | 2023-01-16 13:29:43 -0800 |
---|---|---|
committer | Jeremy Evans <code@jeremyevans.net> | 2023-03-24 11:18:57 -0700 |
commit | 466ca7ae205126c7cac83735db887d69e293f816 (patch) | |
tree | 2d2598ac52e1853f6afb8ddd0b1337616fd2647d /dir.c | |
parent | 5d6579bd9129cfbd62702fb42b249338807a34a2 (diff) |
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]
Notes
Notes:
Merged: https://github.com/ruby/ruby/pull/7135
Diffstat (limited to 'dir.c')
-rw-r--r-- | dir.c | 130 |
1 files changed, 130 insertions, 0 deletions
@@ -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); |