Ruby previously had an emulated approach for File.realpath, which did not work correctly when using unveil(2). This backports a patch to use realpath(3) for File.realpath that I recently committed upstream.
I have tested this works as expected with unveil(2) on -current, and have been running it on some personal apps for about a week to serve Ruby web applications using unveil(2) instead of chroot(2) to limit file system access. unveil(2) is a lot less fragile than chroot(2) for limiting file system access in Ruby web applications, because many Ruby libraries have an unfortunate tendency to load Ruby code at runtime from locations under /usr/local/lib/ruby due to a misfeature called autoload. Regen patches while here. I plan to commit this in a couple days unless I hear objections. Thanks, Jeremy Index: 2.4/Makefile =================================================================== RCS file: /cvs/ports/lang/ruby/2.4/Makefile,v retrieving revision 1.16 diff -u -p -r1.16 Makefile --- 2.4/Makefile 3 Apr 2019 17:25:25 -0000 1.16 +++ 2.4/Makefile 26 Jun 2019 19:45:33 -0000 @@ -3,6 +3,7 @@ VERSION = 2.4.6 SHARED_LIBS = ruby24 2.0 NEXTVER = 2.5 +REVISION-main = 0 PSEUDO_FLAVORS= no_ri_docs bootstrap # Do not build the RI docs on slow arches Index: 2.4/patches/patch-file_c =================================================================== RCS file: 2.4/patches/patch-file_c diff -N 2.4/patches/patch-file_c --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ 2.4/patches/patch-file_c 26 Jun 2019 20:11:48 -0000 @@ -0,0 +1,102 @@ +$OpenBSD$ + +Backport use of realpath(3) for File.realpath to allow unveil(2) to work. + +Index: file.c +--- file.c.orig ++++ file.c +@@ -126,6 +126,9 @@ int flock(int, int); + #define STAT(p, s) stat((p), (s)) + #endif + ++#include <limits.h> ++#include <stdlib.h> ++ + VALUE rb_cFile; + VALUE rb_mFileTest; + VALUE rb_cStat; +@@ -3898,7 +3901,7 @@ realpath_rec(long *prefixlenp, VALUE *resolvedp, const + } + + static VALUE +-rb_check_realpath_internal(VALUE basedir, VALUE path, enum rb_realpath_mode mode) ++rb_check_realpath_emulate(VALUE basedir, VALUE path, enum rb_realpath_mode mode) + { + long prefixlen; + VALUE resolved; +@@ -3980,6 +3983,75 @@ rb_check_realpath_internal(VALUE basedir, VALUE path, + rb_enc_associate(resolved, origenc); + + OBJ_INFECT(resolved, unresolved_path); ++ return resolved; ++} ++ ++static VALUE rb_file_join(VALUE ary, VALUE sep); ++ ++static VALUE ++rb_check_realpath_internal(VALUE basedir, VALUE path, enum rb_realpath_mode mode) ++{ ++ VALUE unresolved_path; ++ rb_encoding *origenc; ++ char *resolved_ptr = NULL; ++ VALUE resolved; ++ ++ if (mode == RB_REALPATH_DIR) { ++ return rb_check_realpath_emulate(basedir, path, mode); ++ } ++ ++ unresolved_path = rb_str_dup_frozen(path); ++ origenc = rb_enc_get(unresolved_path); ++ if (*RSTRING_PTR(unresolved_path) != '/' && !NIL_P(basedir)) { ++ unresolved_path = rb_file_join(rb_ary_new_from_args(2, basedir, unresolved_path), rb_str_new2("/")); ++ } ++ ++ if((resolved_ptr = realpath(RSTRING_PTR(unresolved_path), NULL)) == NULL) { ++ /* glibc realpath(3) does not allow /path/to/file.rb/../other_file.rb, ++ returning ENOTDIR in that case. ++ glibc realpath(3) can also return ENOENT for paths that exist, ++ such as /dev/fd/5. ++ Fallback to the emulated approach in either of those cases. */ ++ if (errno == ENOTDIR || ++ (errno == ENOENT && rb_file_exist_p(0, unresolved_path))) { ++ return rb_check_realpath_emulate(basedir, path, mode); ++ ++ } ++ if (mode == RB_REALPATH_CHECK) { ++ return Qnil; ++ } ++ rb_sys_fail_path(unresolved_path); ++ } ++ resolved = ospath_new(resolved_ptr, strlen(resolved_ptr), rb_filesystem_encoding()); ++ free(resolved_ptr); ++ ++ if (mode == RB_REALPATH_STRICT || mode == RB_REALPATH_CHECK) { ++ struct stat st; ++ ++ if (rb_stat(resolved, &st) < 0) { ++ if (mode == RB_REALPATH_STRICT) { ++ rb_sys_fail_path(unresolved_path); ++ } ++ return Qnil; ++ } ++ } ++ ++ if (origenc != rb_enc_get(resolved)) { ++ if (!rb_enc_str_asciionly_p(resolved)) { ++ resolved = rb_str_conv_enc(resolved, NULL, origenc); ++ } ++ rb_enc_associate(resolved, origenc); ++ } ++ ++ if(rb_enc_str_coderange(resolved) == ENC_CODERANGE_BROKEN) { ++ rb_enc_associate(resolved, rb_filesystem_encoding()); ++ if(rb_enc_str_coderange(resolved) == ENC_CODERANGE_BROKEN) { ++ rb_enc_associate(resolved, rb_ascii8bit_encoding()); ++ } ++ } ++ ++ rb_obj_taint(resolved); ++ RB_GC_GUARD(unresolved_path); + return resolved; + } + Index: 2.5/Makefile =================================================================== RCS file: /cvs/ports/lang/ruby/2.5/Makefile,v retrieving revision 1.8 diff -u -p -r1.8 Makefile --- 2.5/Makefile 15 Mar 2019 16:45:36 -0000 1.8 +++ 2.5/Makefile 26 Jun 2019 19:20:45 -0000 @@ -3,6 +3,7 @@ VERSION = 2.5.5 SHARED_LIBS = ruby25 0.0 NEXTVER = 2.6 +REVISION-main = 0 PSEUDO_FLAVORS= no_ri_docs bootstrap # Do not build the RI docs on slow arches Index: 2.5/patches/patch-configure =================================================================== RCS file: /cvs/ports/lang/ruby/2.5/patches/patch-configure,v retrieving revision 1.2 diff -u -p -r1.2 patch-configure --- 2.5/patches/patch-configure 31 Mar 2018 21:12:45 -0000 1.2 +++ 2.5/patches/patch-configure 26 Jun 2019 19:21:00 -0000 @@ -14,7 +14,7 @@ in earlier ruby versions). Index: configure --- configure.orig +++ configure -@@ -19989,14 +19989,14 @@ fi +@@ -19991,14 +19991,14 @@ fi if test $rb_cv_page_size_log != no; then : cat >>confdefs.h <<_ACEOF @@ -31,7 +31,7 @@ Index: configure _ACEOF -@@ -26268,7 +26268,7 @@ fi +@@ -26270,7 +26270,7 @@ fi openbsd*|mirbsd*) : SOLIBS='$(LIBS)' @@ -40,7 +40,7 @@ Index: configure ;; #( solaris*) : -@@ -27743,7 +27743,7 @@ _ACEOF +@@ -27745,7 +27745,7 @@ _ACEOF else Index: 2.5/patches/patch-ext_openssl_extconf_rb =================================================================== RCS file: /cvs/ports/lang/ruby/2.5/patches/patch-ext_openssl_extconf_rb,v retrieving revision 1.1 diff -u -p -r1.1 patch-ext_openssl_extconf_rb --- 2.5/patches/patch-ext_openssl_extconf_rb 23 Feb 2018 09:54:25 -0000 1.1 +++ 2.5/patches/patch-ext_openssl_extconf_rb 26 Jun 2019 19:21:00 -0000 @@ -3,7 +3,7 @@ $OpenBSD: patch-ext_openssl_extconf_rb,v Index: ext/openssl/extconf.rb --- ext/openssl/extconf.rb.orig +++ ext/openssl/extconf.rb -@@ -134,6 +134,7 @@ have_func("HMAC_CTX_free") +@@ -144,6 +144,7 @@ have_func("HMAC_CTX_free") OpenSSL.check_func("RAND_pseudo_bytes", "openssl/rand.h") # deprecated have_func("X509_STORE_get_ex_data") have_func("X509_STORE_set_ex_data") Index: 2.5/patches/patch-file_c =================================================================== RCS file: 2.5/patches/patch-file_c diff -N 2.5/patches/patch-file_c --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ 2.5/patches/patch-file_c 26 Jun 2019 19:21:00 -0000 @@ -0,0 +1,112 @@ +$OpenBSD$ + +Backport use of realpath(3) for File.realpath to allow unveil(2) to work. + +Index: file.c +--- file.c.orig ++++ file.c +@@ -131,6 +131,9 @@ int flock(int, int); + # define UTIME_EINVAL + #endif + ++#include <limits.h> ++#include <stdlib.h> ++ + VALUE rb_cFile; + VALUE rb_mFileTest; + VALUE rb_cStat; +@@ -4057,7 +4060,7 @@ realpath_rec(long *prefixlenp, VALUE *resolvedp, const + } + + static VALUE +-rb_check_realpath_internal(VALUE basedir, VALUE path, enum rb_realpath_mode mode) ++rb_check_realpath_emulate(VALUE basedir, VALUE path, enum rb_realpath_mode mode) + { + long prefixlen; + VALUE resolved; +@@ -4151,6 +4154,76 @@ rb_check_realpath_internal(VALUE basedir, VALUE path, + return resolved; + } + ++static VALUE rb_file_join(VALUE ary); ++ ++static VALUE ++rb_check_realpath_internal(VALUE basedir, VALUE path, enum rb_realpath_mode mode) ++{ ++ VALUE unresolved_path; ++ rb_encoding *origenc; ++ char *resolved_ptr = NULL; ++ VALUE resolved; ++ ++ if (mode == RB_REALPATH_DIR) { ++ return rb_check_realpath_emulate(basedir, path, mode); ++ } ++ ++ unresolved_path = rb_str_dup_frozen(path); ++ origenc = rb_enc_get(unresolved_path); ++ if (*RSTRING_PTR(unresolved_path) != '/' && !NIL_P(basedir)) { ++ unresolved_path = rb_file_join(rb_ary_new_from_args(2, basedir, unresolved_path)); ++ } ++ unresolved_path = TO_OSPATH(unresolved_path); ++ ++ if((resolved_ptr = realpath(RSTRING_PTR(unresolved_path), NULL)) == NULL) { ++ /* glibc realpath(3) does not allow /path/to/file.rb/../other_file.rb, ++ returning ENOTDIR in that case. ++ glibc realpath(3) can also return ENOENT for paths that exist, ++ such as /dev/fd/5. ++ Fallback to the emulated approach in either of those cases. */ ++ if (errno == ENOTDIR || ++ (errno == ENOENT && rb_file_exist_p(0, unresolved_path))) { ++ return rb_check_realpath_emulate(basedir, path, mode); ++ ++ } ++ if (mode == RB_REALPATH_CHECK) { ++ return Qnil; ++ } ++ rb_sys_fail_path(unresolved_path); ++ } ++ resolved = ospath_new(resolved_ptr, strlen(resolved_ptr), rb_filesystem_encoding()); ++ free(resolved_ptr); ++ ++ if (mode == RB_REALPATH_STRICT || mode == RB_REALPATH_CHECK) { ++ struct stat st; ++ ++ if (rb_stat(resolved, &st) < 0) { ++ if (mode == RB_REALPATH_STRICT) { ++ rb_sys_fail_path(unresolved_path); ++ } ++ return Qnil; ++ } ++ } ++ ++ if (origenc != rb_enc_get(resolved)) { ++ if (!rb_enc_str_asciionly_p(resolved)) { ++ resolved = rb_str_conv_enc(resolved, NULL, origenc); ++ } ++ rb_enc_associate(resolved, origenc); ++ } ++ ++ if(rb_enc_str_coderange(resolved) == ENC_CODERANGE_BROKEN) { ++ rb_enc_associate(resolved, rb_filesystem_encoding()); ++ if(rb_enc_str_coderange(resolved) == ENC_CODERANGE_BROKEN) { ++ rb_enc_associate(resolved, rb_ascii8bit_encoding()); ++ } ++ } ++ ++ rb_obj_taint(resolved); ++ RB_GC_GUARD(unresolved_path); ++ return resolved; ++} ++ + VALUE + rb_realpath_internal(VALUE basedir, VALUE path, int strict) + { +@@ -4572,8 +4645,6 @@ rb_file_s_split(VALUE klass, VALUE path) + FilePathStringValue(path); /* get rid of converting twice */ + return rb_assoc_new(rb_file_dirname(path), rb_file_s_basename(1,&path)); + } +- +-static VALUE rb_file_join(VALUE ary); + + static VALUE + file_inspect_join(VALUE ary, VALUE arg, int recur) Index: 2.6/Makefile =================================================================== RCS file: /cvs/ports/lang/ruby/2.6/Makefile,v retrieving revision 1.5 diff -u -p -r1.5 Makefile --- 2.6/Makefile 27 May 2019 21:42:01 -0000 1.5 +++ 2.6/Makefile 26 Jun 2019 18:02:41 -0000 @@ -5,7 +5,7 @@ DISTNAME = ruby-${VERSION} SHARED_LIBS = ruby26 0.0 NEXTVER = 2.7 -REVISION-main = 0 +REVISION-main = 1 MASTER_SITES0 = https://github.com/ruby/ruby/commit/ PATCHFILES = 1ef39d8d099f145222b9352423af16a2bab6e05b.patch:0 PATCH_DIST_STRIP = -p1 Index: 2.6/patches/patch-file_c =================================================================== RCS file: 2.6/patches/patch-file_c diff -N 2.6/patches/patch-file_c --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ 2.6/patches/patch-file_c 26 Jun 2019 18:02:13 -0000 @@ -0,0 +1,112 @@ +$OpenBSD$ + +Backport use of realpath(3) for File.realpath to allow unveil(2) to work. + +Index: file.c +--- file.c.orig ++++ file.c +@@ -132,6 +132,9 @@ int flock(int, int); + # define UTIME_EINVAL + #endif + ++#include <limits.h> ++#include <stdlib.h> ++ + VALUE rb_cFile; + VALUE rb_mFileTest; + VALUE rb_cStat; +@@ -4064,7 +4067,7 @@ realpath_rec(long *prefixlenp, VALUE *resolvedp, const + } + + static VALUE +-rb_check_realpath_internal(VALUE basedir, VALUE path, enum rb_realpath_mode mode) ++rb_check_realpath_emulate(VALUE basedir, VALUE path, enum rb_realpath_mode mode) + { + long prefixlen; + VALUE resolved; +@@ -4158,6 +4161,76 @@ rb_check_realpath_internal(VALUE basedir, VALUE path, + return resolved; + } + ++static VALUE rb_file_join(VALUE ary); ++ ++static VALUE ++rb_check_realpath_internal(VALUE basedir, VALUE path, enum rb_realpath_mode mode) ++{ ++ VALUE unresolved_path; ++ rb_encoding *origenc; ++ char *resolved_ptr = NULL; ++ VALUE resolved; ++ ++ if (mode == RB_REALPATH_DIR) { ++ return rb_check_realpath_emulate(basedir, path, mode); ++ } ++ ++ unresolved_path = rb_str_dup_frozen(path); ++ origenc = rb_enc_get(unresolved_path); ++ if (*RSTRING_PTR(unresolved_path) != '/' && !NIL_P(basedir)) { ++ unresolved_path = rb_file_join(rb_ary_new_from_args(2, basedir, unresolved_path)); ++ } ++ unresolved_path = TO_OSPATH(unresolved_path); ++ ++ if((resolved_ptr = realpath(RSTRING_PTR(unresolved_path), NULL)) == NULL) { ++ /* glibc realpath(3) does not allow /path/to/file.rb/../other_file.rb, ++ returning ENOTDIR in that case. ++ glibc realpath(3) can also return ENOENT for paths that exist, ++ such as /dev/fd/5. ++ Fallback to the emulated approach in either of those cases. */ ++ if (errno == ENOTDIR || ++ (errno == ENOENT && rb_file_exist_p(0, unresolved_path))) { ++ return rb_check_realpath_emulate(basedir, path, mode); ++ ++ } ++ if (mode == RB_REALPATH_CHECK) { ++ return Qnil; ++ } ++ rb_sys_fail_path(unresolved_path); ++ } ++ resolved = ospath_new(resolved_ptr, strlen(resolved_ptr), rb_filesystem_encoding()); ++ free(resolved_ptr); ++ ++ if (mode == RB_REALPATH_STRICT || mode == RB_REALPATH_CHECK) { ++ struct stat st; ++ ++ if (rb_stat(resolved, &st) < 0) { ++ if (mode == RB_REALPATH_STRICT) { ++ rb_sys_fail_path(unresolved_path); ++ } ++ return Qnil; ++ } ++ } ++ ++ if (origenc != rb_enc_get(resolved)) { ++ if (!rb_enc_str_asciionly_p(resolved)) { ++ resolved = rb_str_conv_enc(resolved, NULL, origenc); ++ } ++ rb_enc_associate(resolved, origenc); ++ } ++ ++ if(rb_enc_str_coderange(resolved) == ENC_CODERANGE_BROKEN) { ++ rb_enc_associate(resolved, rb_filesystem_encoding()); ++ if(rb_enc_str_coderange(resolved) == ENC_CODERANGE_BROKEN) { ++ rb_enc_associate(resolved, rb_ascii8bit_encoding()); ++ } ++ } ++ ++ rb_obj_taint(resolved); ++ RB_GC_GUARD(unresolved_path); ++ return resolved; ++} ++ + VALUE + rb_realpath_internal(VALUE basedir, VALUE path, int strict) + { +@@ -4579,8 +4652,6 @@ rb_file_s_split(VALUE klass, VALUE path) + FilePathStringValue(path); /* get rid of converting twice */ + return rb_assoc_new(rb_file_dirname(path), rb_file_s_basename(1,&path)); + } +- +-static VALUE rb_file_join(VALUE ary); + + static VALUE + file_inspect_join(VALUE ary, VALUE arg, int recur)