├── .gitignore ├── ext └── pledge │ ├── extconf.rb │ └── pledge.c ├── CHANGELOG ├── .github └── workflows │ └── ci.yml ├── Rakefile ├── spec ├── coverage_helper.rb ├── unveil_spec.rb └── pledge_spec.rb ├── MIT-LICENSE ├── pledge.gemspec ├── lib └── unveil.rb └── README.rdoc /.gitignore: -------------------------------------------------------------------------------- 1 | /pledge-*.gem 2 | /lib/pledge.so 3 | /tmp 4 | /coverage 5 | /spec/[0-9]*_test.rb 6 | -------------------------------------------------------------------------------- /ext/pledge/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | have_header 'unistd.h' 3 | have_func('pledge') 4 | have_func('unveil') 5 | $CFLAGS << " -O0 -g -ggdb" if ENV['DEBUG'] 6 | $CFLAGS << " -Wall" 7 | create_makefile("pledge") 8 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | === 1.3.0 (2022-12-19) 2 | 3 | * Allow installation on platforms not supporting pledge, raising NotImplementedError when calling pledge/unveil methods in that case (jcs) (#3) 4 | 5 | === 1.2.0 (2019-07-07) 6 | 7 | * Add unveil library and Pledge.unveil for access to unveil(2) to control file system access (jeremyevans) 8 | 9 | === 1.1.0 (2019-04-25) 10 | 11 | * Support execpromises as optional second argument to Pledge.pledge (jeremyevans) 12 | 13 | === 1.0.0 (2016-11-04) 14 | 15 | * Initial Public Release 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | name: 3.3 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: OpenBSD Test 19 | id: test 20 | uses: vmactions/openbsd-vm@v1 21 | with: 22 | release: 7.6 23 | prepare: | 24 | pkg_add ruby%3.3 25 | gem33 install -N rake-compiler minitest-global_expectations 26 | run: | 27 | ftp -o - https://cdn.openbsd.org/pub/OpenBSD/7.6/amd64/comp76.tgz | tar zxpf - -C / 28 | rake33 29 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/clean" 2 | 3 | CLEAN.include %w'lib/*.so tmp coverage' 4 | 5 | desc "Build the gem" 6 | task :package do 7 | sh %{gem build pledge.gemspec} 8 | end 9 | 10 | desc "Run specs" 11 | task :spec => :compile do 12 | sh %{#{FileUtils::RUBY} #{"-w" if RUBY_VERSION >= '3'} #{'-W:strict_unused_block' if RUBY_VERSION >= '3.4'} spec/pledge_spec.rb} 13 | end 14 | 15 | desc "Run specs" 16 | task :default => :spec 17 | 18 | desc "Run specs with coverage" 19 | task :spec_cov => [:compile] do 20 | ruby = ENV['RUBY'] ||= FileUtils::RUBY 21 | ENV['COVERAGE'] = '1' 22 | FileUtils.rm_rf('coverage') 23 | sh %{#{ruby} spec/unveil_spec.rb} 24 | end 25 | 26 | begin 27 | require 'rake/extensiontask' 28 | Rake::ExtensionTask.new('pledge') 29 | rescue LoadError 30 | end 31 | -------------------------------------------------------------------------------- /spec/coverage_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | 3 | SimpleCov.instance_exec do 4 | enable_coverage :branch 5 | add_filter{|f| f.filename.match(%r{\A#{Regexp.escape(File.dirname(__FILE__))}/})} 6 | add_group('Missing'){|src| src.covered_percent < 100} 7 | add_group('Covered'){|src| src.covered_percent == 100} 8 | enable_for_subprocesses true 9 | 10 | at_fork do |pid| 11 | command_name "#{SimpleCov.command_name} (subprocess: #{pid})" 12 | self.print_error_status = false 13 | formatter SimpleCov::Formatter::SimpleFormatter 14 | minimum_coverage 0 15 | start rescue nil 16 | end 17 | 18 | if ENV['COVERAGE'] == 'subprocess' 19 | command_name 'spawn' 20 | at_fork.call(Process.pid) 21 | else 22 | ENV['COVERAGE'] = 'subprocess' 23 | ENV['RUBYOPT'] = "#{ENV['RUBYOPT']} -r ./spec/coverage_helper" 24 | start 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016,2019 Jeremy Evans 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /pledge.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'pledge' 3 | s.version = '1.3.0' 4 | s.platform = Gem::Platform::RUBY 5 | s.extra_rdoc_files = ["README.rdoc", "CHANGELOG", "MIT-LICENSE"] 6 | s.rdoc_options += ["--quiet", "--line-numbers", "--inline-source", '--title', 'pledge: restrict system operations and file system access on OpenBSD', '--main', 'README.rdoc'] 7 | s.summary = "Restrict system operations and file system access on OpenBSD" 8 | s.author = "Jeremy Evans" 9 | s.email = "code@jeremyevans.net" 10 | s.homepage = "https://github.com/jeremyevans/ruby-pledge" 11 | s.required_ruby_version = ">= 1.9.2" 12 | s.files = %w(MIT-LICENSE CHANGELOG README.rdoc Rakefile ext/pledge/extconf.rb ext/pledge/pledge.c spec/pledge_spec.rb lib/unveil.rb) 13 | s.license = 'MIT' 14 | s.extensions << 'ext/pledge/extconf.rb' 15 | s.description = <''} 31 | end 32 | 33 | unveil_without_lock(paths) 34 | _finalize_unveil! 35 | end 36 | 37 | # Same as unveil, but allows for future calls to unveil or unveil_without_lock. 38 | def unveil_without_lock(paths) 39 | # :nocov: 40 | raise NotImplementedError, "unveil not supported" unless Pledge.respond_to?(:_unveil, true) 41 | # :nocov: 42 | 43 | paths = Hash[paths] 44 | 45 | paths.to_a.each do |path, perm| 46 | unless path.is_a?(String) 47 | raise UnveilError, "unveil path is not a string: #{path.inspect}" 48 | end 49 | 50 | case perm 51 | when :gem 52 | unless spec = Gem.loaded_specs[path] 53 | raise UnveilError, "cannot unveil gem #{path} as it is not loaded" 54 | end 55 | 56 | paths.delete(path) 57 | paths[spec.full_gem_path] = 'r' 58 | when String 59 | # nothing to do 60 | else 61 | raise UnveilError, "unveil permission is not a string: #{perm.inspect}" 62 | end 63 | end 64 | 65 | paths.each do |path, perm| 66 | _unveil(path, perm) 67 | end 68 | 69 | nil 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /ext/pledge/pledge.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | static VALUE ePledgeInvalidPromise; 6 | static VALUE ePledgePermissionIncreaseAttempt; 7 | static VALUE ePledgeError; 8 | static VALUE ePledgeUnveilError; 9 | 10 | static VALUE rb_pledge(int argc, VALUE* argv, VALUE pledge_class) { 11 | VALUE promises = Qnil; 12 | VALUE execpromises = Qnil; 13 | const char * prom = NULL; 14 | const char * execprom = NULL; 15 | 16 | #ifdef HAVE_PLEDGE 17 | rb_scan_args(argc, argv, "11", &promises, &execpromises); 18 | 19 | if (!NIL_P(promises)) { 20 | SafeStringValue(promises); 21 | promises = rb_str_dup(promises); 22 | 23 | /* required for ruby to work */ 24 | rb_str_cat2(promises, " stdio"); 25 | promises = rb_funcall(promises, rb_intern("strip"), 0); 26 | SafeStringValue(promises); 27 | prom = RSTRING_PTR(promises); 28 | } 29 | 30 | if (!NIL_P(execpromises)) { 31 | SafeStringValue(execpromises); 32 | execprom = RSTRING_PTR(execpromises); 33 | } 34 | 35 | if (pledge(prom, execprom) != 0) { 36 | switch(errno) { 37 | case EINVAL: 38 | rb_raise(ePledgeInvalidPromise, "invalid promise in promises string"); 39 | case EPERM: 40 | rb_raise(ePledgePermissionIncreaseAttempt, "attempt to increase permissions"); 41 | default: 42 | rb_raise(ePledgeError, "pledge error"); 43 | } 44 | } 45 | #else 46 | rb_raise(rb_eNotImpError, "pledge not supported"); 47 | #endif 48 | 49 | return Qnil; 50 | } 51 | 52 | #ifdef HAVE_UNVEIL 53 | static VALUE check_unveil(const char * path, const char * perm) { 54 | if (unveil(path, perm) != 0) { 55 | switch(errno) { 56 | case EINVAL: 57 | rb_raise(ePledgeUnveilError, "invalid permissions value"); 58 | case EPERM: 59 | rb_raise(ePledgeUnveilError, "attempt to increase permissions, path not accessible, or unveil already locked"); 60 | case E2BIG: 61 | rb_raise(ePledgeUnveilError, "per-process limit for unveiled paths reached"); 62 | case ENOENT: 63 | rb_raise(ePledgeUnveilError, "directory in the path does not exist"); 64 | default: 65 | rb_raise(ePledgeUnveilError, "unveil error"); 66 | } 67 | } 68 | 69 | return Qnil; 70 | } 71 | 72 | static VALUE rb_unveil(VALUE pledge_class, VALUE path, VALUE perm) { 73 | SafeStringValue(path); 74 | SafeStringValue(perm); 75 | return check_unveil(RSTRING_PTR(path), RSTRING_PTR(perm)); 76 | } 77 | 78 | static VALUE rb_finalize_unveil(VALUE pledge_class) { 79 | return check_unveil(NULL, NULL); 80 | } 81 | #endif 82 | 83 | void Init_pledge(void) { 84 | VALUE cPledge; 85 | cPledge = rb_define_module("Pledge"); 86 | rb_define_method(cPledge, "pledge", rb_pledge, -1); 87 | rb_extend_object(cPledge, cPledge); 88 | ePledgeError = rb_define_class_under(cPledge, "Error", rb_eStandardError); 89 | ePledgeInvalidPromise = rb_define_class_under(cPledge, "InvalidPromise", ePledgeError); 90 | ePledgePermissionIncreaseAttempt = rb_define_class_under(cPledge, "PermissionIncreaseAttempt", ePledgeError); 91 | 92 | #ifdef HAVE_UNVEIL 93 | rb_define_private_method(cPledge, "_unveil", rb_unveil, 2); 94 | rb_define_private_method(cPledge, "_finalize_unveil!", rb_finalize_unveil, 0); 95 | ePledgeUnveilError = rb_define_class_under(cPledge, "UnveilError", rb_eStandardError); 96 | #endif 97 | } 98 | -------------------------------------------------------------------------------- /spec/unveil_spec.rb: -------------------------------------------------------------------------------- 1 | if ENV.delete('COVERAGE') 2 | Dir.mkdir('coverage') unless File.directory?('coverage') 3 | require_relative 'coverage_helper' 4 | end 5 | 6 | require 'rbconfig' 7 | require_relative '../lib/pledge' 8 | 9 | ENV['MT_NO_PLUGINS'] = '1' # Work around stupid autoloading of plugins 10 | gem 'minitest' 11 | require 'minitest/global_expectations/autorun' 12 | 13 | RUBY = RbConfig.ruby 14 | 15 | describe "Pledge.unveil" do 16 | if ENV['COVERAGE'] 17 | def unveil_code(code) 18 | <'r'}, "exit(!Dir['*'].empty?)").must_equal true 61 | 62 | test_read = "exit(((File.read('MIT-LICENSE'); true) rescue false))" 63 | unveiled({'.'=>'w'}, test_read).must_equal false 64 | unveiled({'.'=>'r'}, test_read).must_equal true 65 | unveiled({'.'=>'r'}, "exit(((File.open('MIT-LICENSE', 'w'){}; true) rescue false))").must_equal false 66 | 67 | %w'rwxc rwx rwc rxc rx rw rc'.each do |perm| 68 | unveiled({'.'=>perm}, test_read).must_equal true 69 | end 70 | 71 | %w'wxc wx wc xc x w c'.each do |perm| 72 | unveiled({'.'=>perm}, test_read).must_equal false 73 | end 74 | 75 | unveiled({'MIT-LICENSE'=>'r'}, test_read).must_equal true 76 | unveiled({'Rakefile'=>'r'}, test_read).must_equal false 77 | unveiled({'.'=>'r', 'MIT-LICENSE'=>''}, test_read).must_equal false 78 | unveiled({}, "Pledge.unveil{} rescue exit(1)").must_equal false 79 | run_unveil("Pledge.unveil_without_lock({'.'=>'r'}); Pledge.unveil({}); #{test_read}").must_equal true 80 | run_unveil("Pledge.unveil('foo/bar'=>'r') rescue exit(1)").must_equal false 81 | run_unveil("Pledge.send(:_unveil, '.', 'f') rescue exit!(1)").must_equal false 82 | run_unveil("Pledge.unveil({1=>'s'}) rescue exit(1)").must_equal false 83 | run_unveil("Pledge.unveil({'s'=>1}) rescue exit(1)").must_equal false 84 | run_unveil("Pledge.send(:_unveil, 1, 'r') rescue exit!(1)").must_equal false 85 | run_unveil("Pledge.send(:_unveil, '.', 1) rescue exit!(1)").must_equal false 86 | end 87 | 88 | it "should handle require after unveil with read access after removing from $LOADED_FEATURES" do 89 | [File.join('.', test_file), File.join(Dir.pwd, test_file)].each do |f| 90 | f = f.inspect 91 | run_unveil(<<-END).must_equal true 92 | File.open(#{f}, 'w'){|f| f.write '1'} 93 | require #{f} 94 | Pledge.unveil('spec'=>'r') 95 | $LOADED_FEATURES.delete #{f} 96 | require #{f} 97 | END 98 | end 99 | end 100 | 101 | it "should handle :gem value to unveil gems" do 102 | run_unveil("$stderr.reopen('/dev/null', 'w'); require 'rubygems'; gem 'minitest'; require 'minitest'; Pledge.unveil({}); require 'minitest/benchmark' rescue exit(1)").must_equal false 103 | run_unveil("require 'rubygems'; gem 'minitest'; require 'minitest'; Pledge.unveil('minitest'=>:gem, '.'=>'r'); require 'minitest/benchmark' rescue (p $!; puts $!.backtrace; exit(1))").must_equal true 104 | 105 | run_unveil("Pledge.unveil('gadzooks!!!'=>:gem) rescue exit(1)").must_equal false 106 | end 107 | 108 | it "should need create and write access for writing new files, and create access for removing files" do 109 | unveiled({'.'=>'w'}, "File.open(#{test_file.inspect}, 'w'){|f| f.write '1'} rescue exit(1)").must_equal false 110 | File.file?(test_file).must_equal false 111 | unveiled({'.'=>'c'}, "File.open(#{test_file.inspect}, 'w'){|f| f.write '1'} rescue exit(1)").must_equal false 112 | File.file?(test_file).must_equal false 113 | unveiled({'.'=>'cw'}, "File.open(#{test_file.inspect}, 'w'){|f| f.write '1'}").must_equal true 114 | File.read(test_file).must_equal '1' 115 | 116 | unveiled({'.'=>'w'}, "File.delete(#{test_file.inspect}) rescue exit(1)").must_equal false 117 | File.read(test_file).must_equal '1' 118 | unveiled({'.'=>'c'}, "File.delete(#{test_file.inspect})").must_equal true 119 | File.file?(test_file).must_equal false 120 | end 121 | end if Pledge.respond_to?(:_unveil, true) 122 | -------------------------------------------------------------------------------- /spec/pledge_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'unveil_spec' 2 | 3 | describe "Pledge.pledge" do 4 | def execpledged(promises, execpromises, code) 5 | system(RUBY, '--disable-gems', '-I', 'lib', '-r', 'pledge', '-e', "Pledge.pledge(#{promises.inspect}, #{execpromises.inspect}); #{code}") 6 | end 7 | 8 | def _pledged(status, promises, code) 9 | system(RUBY, '--disable-gems', '-I', 'lib', '-r', 'pledge', '-e', "Pledge.pledge(#{promises.inspect}); #{code}").must_equal status 10 | end 11 | 12 | def pledged(code, promises="") 13 | _pledged(true, promises, code) 14 | end 15 | 16 | def unpledged(code, promises="") 17 | _pledged(false, promises, code) 18 | end 19 | 20 | def with_lib(*libs) 21 | rubyopt = ENV['RUBYOPT'] 22 | ENV['RUBYOPT'] = "#{rubyopt} #{libs.map{|lib| "-r#{lib}"}.join(' ')}" 23 | yield 24 | ensure 25 | ENV['RUBYOPT'] = rubyopt 26 | end 27 | 28 | after do 29 | Dir['spec/_*'].each{|f| File.delete(f)} 30 | Dir['*.core'].each{|f| File.delete(f)} 31 | end 32 | 33 | it "should raise a Pledge::InvalidPromise for unsupported promises" do 34 | proc{Pledge.pledge("foo")}.must_raise Pledge::InvalidPromise 35 | end 36 | 37 | it "should raise a Pledge::PermissionIncreaseAttempt if attempting to increase permissions" do 38 | pledged("begin; Pledge.pledge('rpath'); rescue Pledge::PermissionIncreaseAttempt; exit 0; end; exit 1") 39 | end 40 | 41 | it "should produce a core file on failure" do 42 | unpledged("File.read('#{__FILE__}')") 43 | Dir['*.core'].wont_equal [] 44 | end 45 | 46 | it "should allow reading files if rpath is used" do 47 | unpledged("File.read('#{__FILE__}')") 48 | pledged("File.read('#{__FILE__}')", "rpath") 49 | end 50 | 51 | it "should allow creating files if cpath and wpath are used" do 52 | unpledged("File.open('spec/_test', 'w'){}") 53 | unpledged("File.open('spec/_test', 'w'){}", "cpath") 54 | unpledged("File.open('spec/_test', 'w'){}", "wpath") 55 | File.file?('spec/_test').must_equal false 56 | pledged("File.open('#{'spec/_test'}', 'w'){}", "cpath wpath") 57 | File.file?('spec/_test').must_equal true 58 | end 59 | 60 | it "should allow writing to files if wpath and rpath are used" do 61 | File.open('spec/_test', 'w'){} 62 | unpledged("File.open('spec/_test', 'r+'){}") 63 | pledged("File.open('#{'spec/_test'}', 'r+'){|f| f.write '1'}", "wpath rpath") 64 | File.read('spec/_test').must_equal '1' 65 | end 66 | 67 | it "should allow dns lookups if dns is used" do 68 | with_lib('socket') do 69 | unpledged("Addrinfo.getaddrinfo('google.com', nil)") 70 | pledged("Addrinfo.getaddrinfo('google.com', nil)", "dns") 71 | end 72 | end 73 | 74 | it "should allow internet access if inet is used" do 75 | with_lib('rubygems', 'net/http') do 76 | unpledged("Net::HTTP.get('127.0.0.1', '/index.html') rescue nil") 77 | promises = "inet" 78 | # rpath necessary on ruby < 2.1, as it calls stat 79 | promises << " rpath" if RUBY_VERSION < '2.1' 80 | pledged("Net::HTTP.get('127.0.0.1', '/index.html') rescue nil", promises) 81 | end 82 | end 83 | 84 | it "should allow killing programs if proc is used" do 85 | unpledged("Process.kill(:URG, #{$$})") 86 | pledged("Process.kill(:URG, #{$$})", "proc") 87 | end 88 | 89 | it "should allow creating temp files if tmppath and rpath are used" do 90 | with_lib('tempfile') do 91 | unpledged("Tempfile.new('foo')") 92 | unpledged("Tempfile.new('foo')", "tmppath") 93 | unpledged("Tempfile.new('foo')", "rpath") 94 | promises = "tmppath rpath" 95 | # cpath necessary on ruby < 2.0, as it calls mkdir 96 | promises << " cpath" if RUBY_VERSION < '2.0' 97 | pledged("Tempfile.new('foo')", promises) 98 | end 99 | end 100 | 101 | it "should allow unix sockets if unix and rpath is used" do 102 | require 'socket' 103 | us = UNIXServer.new('spec/_sock') 104 | with_lib('socket') do 105 | unpledged("UNIXSocket.new('spec/_sock').send('u', 0)") 106 | pledged("UNIXSocket.new('spec/_sock').send('t', 0)", "unix") 107 | end 108 | us.accept.read.must_equal 't' 109 | end 110 | 111 | it "should raise ArgumentError if given in invalid number of arguments" do 112 | proc{Pledge.pledge()}.must_raise ArgumentError 113 | proc{Pledge.pledge("", "", "")}.must_raise ArgumentError 114 | end 115 | 116 | it "should handle both promises and execpromises arguments" do 117 | execpledged("proc exec rpath", "stdio rpath", "exit(`cat MIT-LICENSE` == File.read('MIT-LICENSE'))").must_equal true 118 | execpledged("proc exec", "stdio rpath", "$stderr.reopen('/dev/null', 'w'); exit(`cat MIT-LICENSE` == File.read('MIT-LICENSE'))").must_equal false 119 | execpledged("proc exec rpath", "stdio", "$stderr.reopen('/dev/null', 'w'); exit(`cat MIT-LICENSE` == File.read('MIT-LICENSE'))").must_equal false 120 | end 121 | 122 | it "should handle nil arguments" do 123 | Pledge.pledge(nil).must_be_nil 124 | Pledge.pledge(nil, nil).must_be_nil 125 | execpledged("proc exec rpath", nil, "`cat MIT-LICENSE`").must_equal true 126 | execpledged("", nil, "`cat MIT-LICENSE`").must_equal false 127 | execpledged(nil, "stdio rpath", "`cat MIT-LICENSE`").must_equal true 128 | execpledged(nil, "stdio", "File.read('MIT-LICENSE')").must_equal true 129 | execpledged(nil, "stdio", "$stderr.reopen('/dev/null', 'w'); exit(`cat MIT-LICENSE` == File.read('MIT-LICENSE'))").must_equal false 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = pledge 2 | 3 | pledge exposes OpenBSD's pledge(2) and unveil(2) system 4 | calls to ruby. pledge(2) allows a program to restrict the 5 | types of operations the program can do, and unveil(2) 6 | restricts access to the file system. 7 | 8 | Unlike other similar systems, pledge and unveil are 9 | designed for programs that need to use a wide variety of 10 | operations and file access on initialization, but 11 | a fewer number after initialization (when user input will 12 | be accepted). 13 | 14 | == pledge 15 | 16 | First, you need to require the library 17 | 18 | require 'pledge' 19 | 20 | Then you can use +Pledge.pledge+ as the interface to the pledge(2) 21 | system call. You pass +Pledge.pledge+ a string containing tokens 22 | for the operations you would like to allow (called promises). 23 | For example, if you want to give the process the ability to read 24 | from the file system, but not write to the file system or 25 | allow network access: 26 | 27 | Pledge.pledge("rpath") 28 | 29 | To allow read/write filesystem access, but not network access: 30 | 31 | Pledge.pledge("rpath wpath cpath") 32 | 33 | To allow inet/unix socket access and DNS queries, but not 34 | filesystem access: 35 | 36 | Pledge.pledge("inet unix dns") 37 | 38 | If you want to use pledging in a console application such as 39 | irb or pry, you must include the tty promise: 40 | 41 | Pledge.pledge("tty rpath") 42 | 43 | You can pass a second string argument containing tokens for 44 | the operations you would like to allow in spawned processes 45 | (called execpromises). To allow spawning processes that have 46 | read/write filesystem access only, but not network access: 47 | 48 | Pledge.pledge("proc exec rpath", "stdio rpath wpath cpath") 49 | 50 | +Pledge+ is a module that extends itself, you can include it 51 | in other classes: 52 | 53 | Object.send(:include, Pledge) 54 | pledge("rpath") 55 | 56 | See the pledge(2) man page for a description of the allowed 57 | promises in the strings passed to +Pledge.pledge+. 58 | 59 | Using an unsupported promise will raise an exception. The "stdio" 60 | promise is added automatically to the current process's promises, 61 | as ruby does not function without it, but it is not added to 62 | the execpromises (as you can execute non-ruby programs). 63 | 64 | == unveil 65 | 66 | First, you need to require the library 67 | 68 | require 'unveil' 69 | 70 | Then you can use +Pledge.unveil+ as the interface to the unveil(2) 71 | system call. You pass +Pledge.unveil+ a hash of paths and permissions, 72 | for those paths, and it calls unveil(2) with the path and permissions 73 | for each entry. 74 | 75 | The permissions should be a string with the following characters: 76 | 77 | r :: Allow read access to existing files and directories 78 | w :: Allow write access to existing files and directories 79 | x :: Allow execute access to programs 80 | c :: Allow create access for new files and directories 81 | 82 | You can use the empty string as permissions if you want to allow no access 83 | to the given path, even if you have granted some access to a folder above 84 | the given folder. You can use a value of +:gem+ to allow read access to 85 | the directory for the gem specified by the key. 86 | 87 | +Pledge.unveil+ locks the file system access to the specified paths. If 88 | you want to specify which paths to allow in multiple places in your 89 | program, use +Pledge.unveil_without_lock+ for the initial calls and 90 | +Pledge.unveil+ for the final call. 91 | 92 | If +Pledge.unveil+ is called with an empty hash, it adds an unveil of +/+ 93 | with no permissions, which denies all access to the file system if 94 | +unveil_without_lock+ was not called previously with paths. 95 | 96 | Example: 97 | 98 | Pledge.unveil( 99 | '/home/foo/bar' => 'r', 100 | '/home/foo/bar/data' => 'rwc', 101 | '/bin' => 'x', 102 | '/home/foo/bar/secret' => '', 103 | 'rack' => :gem 104 | ) 105 | 106 | The value of :gem is mostly needed if the gem uses autoload or 107 | other forms of runtime requires. This allows read access to 108 | all files in the gem's folder, not just the gem's require paths, 109 | so it works correctly for gems that access data (e.g. templates) 110 | outside of the gem's require paths. 111 | 112 | If you plan to use pledge and unveil together, you should 113 | unveil before pledging, unless you use the +unveil+ 114 | promise when pledging. 115 | 116 | === Issues with unveil and File.realpath 117 | 118 | +Pledge.unveil+ does not work with +File.realpath+ on Ruby <2.7. 119 | The Ruby ports officially supported by OpenBSD have had support to 120 | allow them to work together backported, as long as you are running 121 | OpenBSD 6.6+ (or 6.5-current after July 2019). As +require+ uses 122 | +File.realpath+, this means in most cases where you would want to 123 | use the +:gem+ support, it will not actually work correctly unless 124 | you are using Ruby 2.7+ or an OpenBSD package with the backported 125 | support. 126 | 127 | == Reporting issues/bugs 128 | 129 | This library uses GitHub Issues for tracking issues/bugs: 130 | 131 | https://github.com/jeremyevans/ruby-pledge/issues 132 | 133 | == Contributing 134 | 135 | The source code is on GitHub: 136 | 137 | https://github.com/jeremyevans/ruby-pledge 138 | 139 | To get a copy: 140 | 141 | git clone git://github.com/jeremyevans/ruby-pledge.git 142 | 143 | == Requirements 144 | 145 | * OpenBSD 5.9+ (6.4+ for unveil, but 6.6+ recommended) 146 | * ruby 1.8.7+ 147 | * rake-compiler (if compiling) 148 | 149 | == Compiling 150 | 151 | To build the library from a git checkout, use the compile task. 152 | 153 | rake compile 154 | 155 | == Running the specs 156 | 157 | The rake spec task runs the specs. This is also the default rake 158 | task. This will compile the library if not already compiled. 159 | 160 | rake 161 | 162 | == Author 163 | 164 | Jeremy Evans 165 | --------------------------------------------------------------------------------