From 9190e0b56accc0bc031f50465b85ac3d1c437b93 Mon Sep 17 00:00:00 2001 From: Barry Allard Date: Sun, 29 Jan 2023 21:14:44 -0600 Subject: [PATCH] Refactor Includes and integrates - https://github.com/ruby/tmpdir/pull/9 - https://github.com/ruby/tmpdir/pull/20 - https://github.com/ruby/tmpdir/pull/21 - https://github.com/ruby/tmpdir/pull/22 --- lib/tmpdir.rb | 104 ++---------------------- lib/tmpdir/insecure_world_writable.rb | 17 ++++ lib/tmpdir/tmpname.rb | 113 ++++++++++++++++++++++++++ test/test_tmpdir.rb | 22 +++++ 4 files changed, 157 insertions(+), 99 deletions(-) create mode 100644 lib/tmpdir/insecure_world_writable.rb create mode 100644 lib/tmpdir/tmpname.rb diff --git a/lib/tmpdir.rb b/lib/tmpdir.rb index 55920a4..b63a299 100644 --- a/lib/tmpdir.rb +++ b/lib/tmpdir.rb @@ -5,37 +5,14 @@ # $Id$ # -require 'fileutils' -begin - require 'etc.so' -rescue LoadError # rescue LoadError for miniruby -end +require_relative './tmpdir/tmpname' class Dir - - @@systmpdir ||= defined?(Etc.systmpdir) ? Etc.systmpdir : '/tmp' - ## # Returns the operating system's temporary file path. - + # def self.tmpdir - ['TMPDIR', 'TMP', 'TEMP', ['system temporary path', @@systmpdir], ['/tmp']*2, ['.']*2].find do |name, dir| - unless dir - next if !(dir = ENV[name]) or dir.empty? - end - dir = File.expand_path(dir) - stat = File.stat(dir) rescue next - case - when !stat.directory? - warn "#{name} is not a directory: #{dir}" - when !stat.writable? - warn "#{name} is not writable: #{dir}" - when stat.world_writable? && !stat.sticky? - warn "#{name} is world-writable: #{dir}" - else - break dir - end - end or raise ArgumentError, "could not find a temporary directory" + Dir::Tmpname.tmpdir end # Dir.mktmpdir creates a temporary directory. @@ -83,78 +60,7 @@ def self.tmpdir # FileUtils.remove_entry dir # end # - def self.mktmpdir(prefix_suffix=nil, *rest, **options) - base = nil - path = Tmpname.create(prefix_suffix || "d", *rest, **options) {|path, _, _, d| - base = d - mkdir(path, 0700) - } - if block_given? - begin - yield path.dup - ensure - unless base - stat = File.stat(File.dirname(path)) - if stat.world_writable? and !stat.sticky? - raise ArgumentError, "parent directory is world writable but not sticky" - end - end - FileUtils.remove_entry path - end - else - path - end - end - - # Temporary name generator - module Tmpname # :nodoc: - module_function - - def tmpdir - Dir.tmpdir - end - - # Unusable characters as path name - UNUSABLE_CHARS = "^,-.0-9A-Z_a-z~" - - # Dedicated random number generator - RANDOM = Random.new - class << RANDOM # :nodoc: - # Maximum random number - MAX = 36**6 # < 0x100000000 - - # Returns new random string upto 6 bytes - def next - rand(MAX).to_s(36) - end - end - private_constant :RANDOM - - # Generates and yields random names to create a temporary name - def create(basename, tmpdir=nil, max_try: nil, **opts) - origdir = tmpdir - tmpdir ||= tmpdir() - n = nil - prefix, suffix = basename - prefix = (String.try_convert(prefix) or - raise ArgumentError, "unexpected prefix: #{prefix.inspect}") - prefix = prefix.delete(UNUSABLE_CHARS) - suffix &&= (String.try_convert(suffix) or - raise ArgumentError, "unexpected suffix: #{suffix.inspect}") - suffix &&= suffix.delete(UNUSABLE_CHARS) - begin - t = Time.now.strftime("%Y%m%d") - path = "#{prefix}#{t}-#{$$}-#{RANDOM.next}"\ - "#{n ? %[-#{n}] : ''}#{suffix||''}" - path = File.join(tmpdir, path) - yield(path, n, opts, origdir) - rescue Errno::EEXIST - n ||= 0 - n += 1 - retry if !max_try or n < max_try - raise "cannot generate temporary name using `#{basename}' under `#{tmpdir}'" - end - path - end + def self.mktmpdir(prefix_suffix=nil, *rest, **options, &block) + Dir::Tmpname.mktmpdir(prefix_suffix, *rest, **options, &block) end end diff --git a/lib/tmpdir/insecure_world_writable.rb b/lib/tmpdir/insecure_world_writable.rb new file mode 100644 index 0000000..609f021 --- /dev/null +++ b/lib/tmpdir/insecure_world_writable.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Dir + module Tmpname + module InsecureWorldWritable + refine File::Stat do + def insecure_world_writable? + world_writable? && !sticky? + end + + def not_insecure_world_writable? + !insecure_world_writable? + end + end + end + end +end diff --git a/lib/tmpdir/tmpname.rb b/lib/tmpdir/tmpname.rb new file mode 100644 index 0000000..8fd944f --- /dev/null +++ b/lib/tmpdir/tmpname.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true +# +# tmpdir - retrieve temporary directory path +# +# $Id$ +# + +require 'fileutils' +begin + require 'etc.so' +rescue LoadError # rescue LoadError for miniruby +end +require_relative './insecure_world_writable' + +class Dir + # Temporary name generator + module Tmpname # :nodoc: + using InsecureWorldWritable + + module_function + + SYSTMPDIR = Etc.systmpdir rescue '/tmp' + private_constant :SYSTMPDIR + + TRY_ENVS = %w{TMPDIR TMP TEMP}.freeze + private_constant :TRY_ENVS + + TRY_DIRS = [['system temporary path', SYSTMPDIR], ['/tmp']*2, ['.']*2].map(&:freeze).freeze + private_constant :TRY_DIRS + + PERM_CHECKS = [ + ['not a directory', :directory? ], + ['not writable' , :writable? ], + ['world-writable' , :not_insecure_world_writable? ]].map(&:freeze).freeze + private_constant :PERM_CHECKS + + def tmpdir + TRY_ENVS.each { |env| d = self.verify_permissions(env, ENV[env]); return d if d } + TRY_DIRS.each { |name, dir| d = self.verify_permissions(name, dir) ; return d if d } + raise ArgumentError, 'could not find a temporary directory' + end + + private_class_method def verify_permissions(name, dir) + return if dir.to_s.empty? + dir = File.expand_path(dir) + stat = File.stat(dir) rescue return + dir if PERM_CHECKS.all? { |msg, ok| stat.send(ok) or warn "#{name} is #{msg}: #{dir}" } + end + + def mktmpdir(prefix_suffix = nil, *rest, **options, &block) + base = nil + path = self.create(prefix_suffix || 'd', *rest, **options) do |path, _, _, d| + base = d + Dir.mkdir(path, 0700) + end + if block_given? + self.mktmpdir_with_block(base, path, block) + else + path + end + end + + private_class_method def mktmpdir_with_block(base, path, block) + block.call(path.dup) + ensure + if !base && File.stat(File.dirname(path))&.insecure_world_writable? + raise ArgumentError, 'parent directory is world writable but not sticky' + end + FileUtils.remove_entry path + end + + # Unusable characters as path name + UNUSABLE_CHARS = '^,-.0-9A-Z_a-z~' + + # Generates and yields random names to create a temporary name + def create(basename, tmpdir = nil, max_try: nil, **opts) + raise ArgumentError, "empty parent path" if tmpdir&.empty? + n = nil + begin + path = generate_path(basename, tmpdir, n) + path = File.join(tmpdir || self.tmpdir, path) + yield(path, n, opts, tmpdir) + path + rescue Errno::EEXIST + n ||= 0 + retry if max_try&.>=(n += 1) + raise "cannot generate temporary name using `#{basename}' under `#{tmpdir}'" + end + end + + private_class_method def generate_path(basename, tmpdir, n) + prefix, suffix = self.make_prefix_suffix(basename) + time = Time.now.strftime('%Y%m%d') + rnd = Random.bytes(4).unpack1('L').to_s(36)[0..5] + '%s%s-%d-%s%s%s' % [prefix, time, $$, rnd, n&.-@, suffix] + end + + private_class_method def make_prefix_suffix(basename) + prefix, suffix = basename + prefix = self.make_str('prefix', prefix) + suffix &&= self.make_str('suffix', suffix) + [prefix, suffix] + end + + private_class_method def make_str(msg, var) + if x = String.try_convert(var) + x.delete(UNUSABLE_CHARS) + else + raise ArgumentError, "unexpected #{msg}: #{var.inspect}" + end + end + end +end diff --git a/test/test_tmpdir.rb b/test/test_tmpdir.rb index eada416..eae0610 100644 --- a/test/test_tmpdir.rb +++ b/test/test_tmpdir.rb @@ -104,6 +104,12 @@ def test_mktmpdir_traversal_array end end + def test_mktmpdir_not_empty_parent + assert_raise(ArgumentError) do + Dir.mktmpdir("foo", "") + end + end + def assert_mktmpdir_traversal Dir.mktmpdir do |target| target = target.chomp('/') + '/' @@ -115,4 +121,20 @@ def assert_mktmpdir_traversal end end end + + def test_ractor + assert_ractor(<<~'end;', require: "tmpdir") + r = Ractor.new do + Dir.mktmpdir() do |d| + Ractor.yield d + Ractor.receive + end + end + dir = r.take + assert_file.directory? dir + r.send true + r.take + assert_file.not_exist? dir + end; + end end