exec_sandbox 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.project +18 -0
- data/.rspec +1 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +39 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +42 -0
- data/VERSION +1 -0
- data/lib/exec_sandbox/sandbox.rb +169 -0
- data/lib/exec_sandbox/spawn.rb +143 -0
- data/lib/exec_sandbox/users.rb +156 -0
- data/lib/exec_sandbox/wait4.rb +85 -0
- data/lib/exec_sandbox.rb +23 -0
- data/spec/exec_sandbox/sandbox_spec.rb +138 -0
- data/spec/exec_sandbox/spawn_spec.rb +327 -0
- data/spec/exec_sandbox/users_spec.rb +125 -0
- data/spec/exec_sandbox/wait4_spec.rb +24 -0
- data/spec/fixtures/buffer.rb +10 -0
- data/spec/fixtures/churn.rb +16 -0
- data/spec/fixtures/duplicate.rb +7 -0
- data/spec/fixtures/exit_arg.rb +5 -0
- data/spec/fixtures/fork.rb +18 -0
- data/spec/fixtures/pwd.rb +8 -0
- data/spec/fixtures/write_arg.rb +8 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/code_fixture.rb +7 -0
- metadata +165 -0
data/.document
ADDED
data/.project
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<projectDescription>
|
3
|
+
<name>exec_sandbox</name>
|
4
|
+
<comment></comment>
|
5
|
+
<projects>
|
6
|
+
</projects>
|
7
|
+
<buildSpec>
|
8
|
+
<buildCommand>
|
9
|
+
<name>com.aptana.ide.core.unifiedBuilder</name>
|
10
|
+
<arguments>
|
11
|
+
</arguments>
|
12
|
+
</buildCommand>
|
13
|
+
</buildSpec>
|
14
|
+
<natures>
|
15
|
+
<nature>com.aptana.ruby.core.rubynature</nature>
|
16
|
+
<nature>com.aptana.projects.webnature</nature>
|
17
|
+
</natures>
|
18
|
+
</projectDescription>
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
source :rubygems
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem 'activesupport', '>= 2.3.5'
|
5
|
+
gem 'ffi', '>= 1.0.9'
|
6
|
+
|
7
|
+
# Add dependencies to develop your gem here.
|
8
|
+
# Include everything needed to run rake, tests, features, etc.
|
9
|
+
group :development do
|
10
|
+
gem 'rdoc', '>= 3.10'
|
11
|
+
gem 'rspec', '>= 2.6.0'
|
12
|
+
gem 'yard', '>= 0.7.2'
|
13
|
+
gem 'yard-rspec', '>= 0.1'
|
14
|
+
gem 'bundler', '>= 1.0.21'
|
15
|
+
gem 'jeweler', '>= 1.6.4'
|
16
|
+
gem 'rcov', '>= 0'
|
17
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
diff-lcs (1.1.3)
|
5
|
+
ffi (1.0.9)
|
6
|
+
git (1.2.5)
|
7
|
+
jeweler (1.6.4)
|
8
|
+
bundler (~> 1.0)
|
9
|
+
git (>= 1.2.5)
|
10
|
+
rake
|
11
|
+
json (1.6.1)
|
12
|
+
rake (0.9.2)
|
13
|
+
rcov (0.9.11)
|
14
|
+
rdoc (3.10)
|
15
|
+
json (~> 1.4)
|
16
|
+
rspec (2.6.0)
|
17
|
+
rspec-core (~> 2.6.0)
|
18
|
+
rspec-expectations (~> 2.6.0)
|
19
|
+
rspec-mocks (~> 2.6.0)
|
20
|
+
rspec-core (2.6.4)
|
21
|
+
rspec-expectations (2.6.0)
|
22
|
+
diff-lcs (~> 1.1.2)
|
23
|
+
rspec-mocks (2.6.0)
|
24
|
+
yard (0.7.2)
|
25
|
+
yard-rspec (0.1)
|
26
|
+
yard
|
27
|
+
|
28
|
+
PLATFORMS
|
29
|
+
ruby
|
30
|
+
|
31
|
+
DEPENDENCIES
|
32
|
+
bundler (>= 1.0.21)
|
33
|
+
ffi (>= 1.0.9)
|
34
|
+
jeweler (>= 1.6.4)
|
35
|
+
rcov
|
36
|
+
rdoc (>= 3.10)
|
37
|
+
rspec (>= 2.6.0)
|
38
|
+
yard (>= 0.7.2)
|
39
|
+
yard-rspec (>= 0.1)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Victor Costan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
= exec_sandbox
|
2
|
+
|
3
|
+
Description goes here.
|
4
|
+
|
5
|
+
== Contributing to exec_sandbox
|
6
|
+
|
7
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
8
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
9
|
+
* Fork the project
|
10
|
+
* Start a feature/bugfix branch
|
11
|
+
* Commit and push until you are happy with your contribution
|
12
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
13
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
14
|
+
|
15
|
+
== Copyright
|
16
|
+
|
17
|
+
Copyright (c) 2011 Victor Costan. See LICENSE.txt for
|
18
|
+
further details.
|
19
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "exec_sandbox"
|
18
|
+
gem.homepage = "http://github.com/pwnall/exec_sandbox"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{Run foreign binaries using POSIX sandboxing features}
|
21
|
+
gem.description = %Q{Temporary users and groups, rlimits}
|
22
|
+
gem.email = "costan@gmail.com"
|
23
|
+
gem.authors = ["Victor Costan"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rspec/core'
|
29
|
+
require 'rspec/core/rake_task'
|
30
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
31
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
32
|
+
end
|
33
|
+
|
34
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
35
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
36
|
+
spec.rcov = true
|
37
|
+
end
|
38
|
+
|
39
|
+
task :default => :spec
|
40
|
+
|
41
|
+
require 'yard'
|
42
|
+
YARD::Rake::YardocTask.new
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# namespace
|
2
|
+
module ExecSandbox
|
3
|
+
|
4
|
+
# Manages sandboxed processes.
|
5
|
+
class Sandbox
|
6
|
+
# The path to the sandbox's working directory.
|
7
|
+
attr_reader :path
|
8
|
+
|
9
|
+
# Empty sandbox.
|
10
|
+
#
|
11
|
+
# @param [String] admin the name of a user who will be able to peek into the
|
12
|
+
# sandbox (optional)
|
13
|
+
def initialize(admin = nil)
|
14
|
+
@user_name = ExecSandbox::Users.temp
|
15
|
+
user_pwd = Etc.getpwnam @user_name
|
16
|
+
@user_uid = user_pwd.uid
|
17
|
+
@user_gid = user_pwd.gid
|
18
|
+
@path = user_pwd.dir
|
19
|
+
if @admin_name = admin
|
20
|
+
admin_pwd = Etc.getpwnam(@admin_name)
|
21
|
+
@admin_uid = admin_pwd.uid
|
22
|
+
@admin_gid = admin_pwd.gid
|
23
|
+
else
|
24
|
+
@admin_uid = @user_uid
|
25
|
+
@admin_gid = @user_gid
|
26
|
+
end
|
27
|
+
@destroyed = false
|
28
|
+
|
29
|
+
# principal argument for Spawn.spawn()
|
30
|
+
@principal = { :uid => @user_uid, :gid => @user_gid, :dir => @path }
|
31
|
+
end
|
32
|
+
|
33
|
+
# Copies a file or directory to the sandbox.
|
34
|
+
#
|
35
|
+
# @param [String] from path to the file or directory to be copied
|
36
|
+
# @param [Hash] options tweaks the permissions and the path inside the sandbox
|
37
|
+
# @option options [String] :to the path inside the sandbox where the file or
|
38
|
+
# directory will be copied (defaults to the name of the source)
|
39
|
+
# @option options [Boolean] :read_only if true, the sandbox user will not be
|
40
|
+
# able to write to the file / directory
|
41
|
+
# @return [String] the absolute path to the copied file / directory inside the
|
42
|
+
# sandbox
|
43
|
+
def push(from, options = {})
|
44
|
+
to = File.join @path, (options[:to] || File.basename(from))
|
45
|
+
FileUtils.cp_r from, to
|
46
|
+
|
47
|
+
permissions = options[:read_only] ? 0770 : 0750
|
48
|
+
FileUtils.chmod_R permissions, to
|
49
|
+
FileUtils.chown_R @admin_uid, @user_gid, to
|
50
|
+
# NOTE: making a file / directory read-only is useless -- the sandboxed
|
51
|
+
# process can replace the file with another copy of the file; this can
|
52
|
+
# be worked around by noting the inode number of the protected file /
|
53
|
+
# dir, and making a hard link to it somewhere else so the inode won't
|
54
|
+
# be reused.
|
55
|
+
|
56
|
+
to
|
57
|
+
end
|
58
|
+
|
59
|
+
# Copies a file or directory from the sandbox.
|
60
|
+
#
|
61
|
+
# @param [String] from relative path to the sandbox file or directory
|
62
|
+
# @param [String] to path where the file/directory will be copied
|
63
|
+
# @param [Hash] options tweaks the permissions and the path inside the sandbox
|
64
|
+
# @return [String] the path to the copied file / directory outside the sandbox
|
65
|
+
def pull(from, to)
|
66
|
+
from = File.join @path, from
|
67
|
+
FileUtils.cp_r from, to
|
68
|
+
|
69
|
+
FileUtils.chmod_R 0770, to
|
70
|
+
FileUtils.chown_R @admin_uid, @admin_gid, to
|
71
|
+
# NOTE: making a file / directory read-only is useless -- the sandboxed
|
72
|
+
# process can replace the file with another copy of the file; this can
|
73
|
+
# be worked around by noting the inode number of the protected file /
|
74
|
+
# dir, and making a hard link to it somewhere else so the inode won't
|
75
|
+
# be reused.
|
76
|
+
|
77
|
+
to
|
78
|
+
end
|
79
|
+
|
80
|
+
# Runs a command in the sandbox.
|
81
|
+
#
|
82
|
+
# @param [Array, String] command to be run; use an array to pass arguments to
|
83
|
+
# the command
|
84
|
+
# @param [Hash] options stdin / stdout redirection and resource limitations
|
85
|
+
# @option options [Hash] :limits see {Spawn#set_limits}
|
86
|
+
# @option options [String] :in path to a file that is set as the child's stdin
|
87
|
+
# @option options [String] :in_data contents to be written to a pipe that is
|
88
|
+
# set as the child's stdin; if neither :in nor :in_data are specified, the
|
89
|
+
# child will receive the read end of an empty pipe
|
90
|
+
# @option options [String] :out path to a file that is set as the child's
|
91
|
+
# stdout; if not set, the child will receive the write end of a pipe whose
|
92
|
+
# contents is returned in :out_data
|
93
|
+
# @return [Hash] the result of {Wait4#wait4}, plus an :out_data key if no :out
|
94
|
+
# option is given
|
95
|
+
def run(command, options = {})
|
96
|
+
limits = options[:limits] || {}
|
97
|
+
|
98
|
+
io = {}
|
99
|
+
if options[:in]
|
100
|
+
io[:in] = options[:in]
|
101
|
+
in_rd = nil
|
102
|
+
else
|
103
|
+
in_rd, in_wr = IO.pipe
|
104
|
+
in_wr.write options[:in_data] if options[:in_data]
|
105
|
+
in_wr.close
|
106
|
+
io[:in] = in_rd
|
107
|
+
end
|
108
|
+
if options[:out]
|
109
|
+
io[:out] = options[:out]
|
110
|
+
else
|
111
|
+
out_rd, out_wr = IO.pipe
|
112
|
+
io[:out] = out_wr
|
113
|
+
end
|
114
|
+
io[:err] = STDERR unless options[:no_stderr]
|
115
|
+
|
116
|
+
pid = ExecSandbox::Spawn.spawn command, io, @principal, limits
|
117
|
+
# Close the pipe ends that are meant to be used in the child.
|
118
|
+
in_rd.close if in_rd
|
119
|
+
out_wr.close if out_wr
|
120
|
+
|
121
|
+
# Collect information about the child.
|
122
|
+
status = ExecSandbox::Wait4.wait4 pid
|
123
|
+
if out_rd
|
124
|
+
status[:out_data] = out_rd.read
|
125
|
+
out_rd.close
|
126
|
+
end
|
127
|
+
status
|
128
|
+
end
|
129
|
+
|
130
|
+
# Removes the files and temporary user associated with this sandbox.
|
131
|
+
def close
|
132
|
+
return if @destroyed
|
133
|
+
ExecSandbox::Users.destroy @user_name
|
134
|
+
@destroyed = true
|
135
|
+
end
|
136
|
+
|
137
|
+
# Cleans up when the sandbox object is garbage-collected.
|
138
|
+
def finalize
|
139
|
+
close
|
140
|
+
end
|
141
|
+
end # module ExecSandbox::Sandbox
|
142
|
+
|
143
|
+
# Creates a sandbox, yields it, and destroys it.
|
144
|
+
#
|
145
|
+
# @param [String] admin the name of a user who will be able to peek into the
|
146
|
+
# sandbox (optional)
|
147
|
+
# @return the value returned from the block passed to this method
|
148
|
+
def self.use(admin = nil, &block)
|
149
|
+
sandbox = ExecSandbox::Sandbox.new admin
|
150
|
+
begin
|
151
|
+
return yield(sandbox)
|
152
|
+
ensure
|
153
|
+
sandbox.close
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Creates a sandbox.
|
158
|
+
#
|
159
|
+
# The sandbox should be disposed of by calling {Sandbox#close} on it. This
|
160
|
+
# method is much less convenient than #use, so make sure you have a good
|
161
|
+
# reason to call it.
|
162
|
+
#
|
163
|
+
# @param [String] admin the name of a user who will be able to peek into the
|
164
|
+
# sandbox (optional)
|
165
|
+
# @return the value returned from the block passed to this method
|
166
|
+
def self.open(admin = nil)
|
167
|
+
ExecSandbox::Sandbox.new admin
|
168
|
+
end
|
169
|
+
end # namespace ExecSandbox
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# namespace
|
2
|
+
module ExecSandbox
|
3
|
+
|
4
|
+
# Manages sandboxed processes.
|
5
|
+
module Spawn
|
6
|
+
# Spawns a child process.
|
7
|
+
#
|
8
|
+
# @param [String, Array] command the command to be executed via exec
|
9
|
+
# @param [Hash] io see limit_io
|
10
|
+
# @param [Hash] principal the principal for the enw process
|
11
|
+
# @param [Hash] resources see limit_resources
|
12
|
+
# @return [Fixnum] the child's PID
|
13
|
+
def self.spawn(command, io = {}, principal = {}, resources = {})
|
14
|
+
fork do
|
15
|
+
limit_io io
|
16
|
+
limit_resources resources
|
17
|
+
set_principal principal
|
18
|
+
if command.respond_to? :to_str
|
19
|
+
Process.exec command
|
20
|
+
else
|
21
|
+
Process.exec *command
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Constraints the available file descriptors.
|
27
|
+
#
|
28
|
+
# @param [Hash] io associates file descriptors with IO objects or file paths;
|
29
|
+
# all file descriptors not covered by io will be closed
|
30
|
+
def self.limit_io(io)
|
31
|
+
[:in, :out, :err].each_with_index do |sym, fd_num|
|
32
|
+
if target = io.delete(sym)
|
33
|
+
io[fd_num] = target
|
34
|
+
end
|
35
|
+
end
|
36
|
+
io.each do |k, v|
|
37
|
+
if v.respond_to?(:fileno)
|
38
|
+
if v.fileno != k
|
39
|
+
LibC.close k
|
40
|
+
LibC.dup2 v.fileno, k
|
41
|
+
end
|
42
|
+
else
|
43
|
+
LibC.close k
|
44
|
+
open_fd = IO.sysopen(v, 'r+')
|
45
|
+
if open_fd != k
|
46
|
+
LibC.dup2 open_fd, k
|
47
|
+
LibC.close open_fd
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Close all file descriptors.
|
53
|
+
max_fd = LibC.getdtablesize
|
54
|
+
0.upto(max_fd) do |fd|
|
55
|
+
next if io[fd]
|
56
|
+
LibC.close fd
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Sets the process' principal for access control.
|
61
|
+
#
|
62
|
+
# @param [Hash] principal information about the process' principal
|
63
|
+
# @option principal [String] :dir the process' working directory
|
64
|
+
# @option principal [Fixnum] :uid the new user ID
|
65
|
+
# @option principal [Fixnum] :gid the new group ID
|
66
|
+
def self.set_principal(principal)
|
67
|
+
Dir.chdir principal[:dir] if principal[:dir]
|
68
|
+
|
69
|
+
if principal[:gid]
|
70
|
+
begin
|
71
|
+
Process::Sys.setresgid principal[:gid], principal[:gid], principal[:gid]
|
72
|
+
rescue NotImplementedError
|
73
|
+
Process.gid = principal[:gid]
|
74
|
+
Process.egid = principal[:gid]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
if principal[:uid]
|
78
|
+
begin
|
79
|
+
Process.initgroups Etc.getpwuid(principal[:uid]).name,
|
80
|
+
principal[:gid] || Process.gid
|
81
|
+
rescue NotImplementedError
|
82
|
+
end
|
83
|
+
|
84
|
+
begin
|
85
|
+
Process::Sys.setresuid principal[:uid], principal[:uid], principal[:uid]
|
86
|
+
rescue NotImplementedError
|
87
|
+
Process.uid = principal[:uid]
|
88
|
+
Process.euid = principal[:uid]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Constrains the resource usage of the current process.
|
94
|
+
#
|
95
|
+
# @param [Hash{Symbol => Number}] limits the constraints to be applied
|
96
|
+
# @option limits [Fixnum] :cpu maximum CPU time (for best results, give it an
|
97
|
+
# extra second, and measure actual resource usage after the process
|
98
|
+
# completes)
|
99
|
+
# @option limits [Fixnum] :processes number of processes that can be spawned
|
100
|
+
# by the user who owns this process (useful in conjunction with temporary
|
101
|
+
# users)
|
102
|
+
# @option limits [Fixnum] :file_size maximum size of a file created by the
|
103
|
+
# process; the process can still fill the disk by creating many files of
|
104
|
+
# this size
|
105
|
+
# @option limits [Fixnum] :open_files maximum number of open files; remember
|
106
|
+
# that any process uses 3 open files for STDIN, STDOUT, and STDERR
|
107
|
+
# @option limits [Fixnum] :data maximum data segment size (static data plus
|
108
|
+
# heap) and stack; allow slack for the libraries used by the process;
|
109
|
+
# mostly useful to prevent a process from freezing the machine by pushing
|
110
|
+
# everything into swap
|
111
|
+
def self.limit_resources(limits)
|
112
|
+
if limits[:cpu]
|
113
|
+
Process.setrlimit Process::RLIMIT_CPU, limits[:cpu], limits[:cpu]
|
114
|
+
end
|
115
|
+
if limits[:processes]
|
116
|
+
Process.setrlimit Process::RLIMIT_NPROC, limits[:processes],
|
117
|
+
limits[:processes]
|
118
|
+
end
|
119
|
+
if limits[:file_size]
|
120
|
+
Process.setrlimit Process::RLIMIT_FSIZE, limits[:file_size],
|
121
|
+
limits[:file_size]
|
122
|
+
end
|
123
|
+
if limits[:open_files]
|
124
|
+
Process.setrlimit Process::RLIMIT_NOFILE, limits[:open_files],
|
125
|
+
limits[:open_files]
|
126
|
+
end
|
127
|
+
if limits[:data]
|
128
|
+
Process.setrlimit Process::RLIMIT_DATA, limits[:data], limits[:data]
|
129
|
+
Process.setrlimit Process::RLIMIT_STACK, limits[:data], limits[:data]
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Maps raw I/O functions.
|
134
|
+
module LibC
|
135
|
+
extend FFI::Library
|
136
|
+
ffi_lib FFI::Library::LIBC
|
137
|
+
attach_function :close, [:int], :int
|
138
|
+
attach_function :getdtablesize, [], :int
|
139
|
+
attach_function :dup2, [:int, :int], :int
|
140
|
+
end # module ExecSandbox::Spawn::Libc
|
141
|
+
end # module ExecSandbox::Spawn
|
142
|
+
|
143
|
+
end # namespace ExecSandbox
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# namespace
|
2
|
+
module ExecSandbox
|
3
|
+
|
4
|
+
# Manages sandbox users.
|
5
|
+
#
|
6
|
+
# @see Users#temp
|
7
|
+
# @see Users#destroy
|
8
|
+
module Users
|
9
|
+
# Creates an unprivileged user.
|
10
|
+
#
|
11
|
+
# @return [String] the user's name
|
12
|
+
def self.temp(prefix = 'xsbx.rb')
|
13
|
+
loop do
|
14
|
+
user_name = prefix + '-%x.%x.%x' % [$PID, Time.now.to_i, rand(65536)]
|
15
|
+
etc = nil
|
16
|
+
begin
|
17
|
+
etc = Etc.getpwnam(name)
|
18
|
+
rescue ArgumentError
|
19
|
+
# User not found: good!
|
20
|
+
end
|
21
|
+
next if etc
|
22
|
+
|
23
|
+
create user_name
|
24
|
+
return user_name
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Creates a user for unprivileged operations.
|
29
|
+
#
|
30
|
+
# @param [String] user_name the user's short (UNIX) name (should be unique)
|
31
|
+
# @param [String] primary_group_name; if no name is supplied, the user's
|
32
|
+
# primary group will be set to a new group
|
33
|
+
#
|
34
|
+
# @return [Fixnum] the new user's UID
|
35
|
+
def self.create(user_name, primary_group_name = nil)
|
36
|
+
group_id = primary_group_name && Etc.getgrnam(primary_group_name).gid
|
37
|
+
|
38
|
+
if RUBY_PLATFORM =~ /darwin/ # OSX
|
39
|
+
home_dir = "/Users/#{user_name}"
|
40
|
+
unless group_id
|
41
|
+
# Create a group with the same name as the user.
|
42
|
+
group_id = `dscl . -list /Groups`.split.
|
43
|
+
map { |g| `dscl . -read /Groups/#{g} PrimaryGroupID`.split.last.
|
44
|
+
to_i }.sort.last + 1
|
45
|
+
|
46
|
+
# Simulate adduser's group creation.
|
47
|
+
command_prefix = ['dscl', '.', '-create', "/Groups/#{user_name}"]
|
48
|
+
[
|
49
|
+
[],
|
50
|
+
['PrimaryGroupID', group_id.to_s],
|
51
|
+
].each do |command_suffix|
|
52
|
+
command = command_prefix + command_suffix
|
53
|
+
unless Kernel.system(*command)
|
54
|
+
raise RuntimeError, "User creation failed at #{command.inspect}!"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Find an available UID.
|
60
|
+
user_id = `dscl . -list /Users`.split.
|
61
|
+
map { |u| `dscl . -read /Users/#{u} UniqueID`.split.last.to_i }.
|
62
|
+
sort.last + 1
|
63
|
+
|
64
|
+
# Simulate adduser.
|
65
|
+
command_prefix = ['dscl', '.', '-create', "/Users/#{user_name}"]
|
66
|
+
[
|
67
|
+
[],
|
68
|
+
['UserShell', '/bin/bash'],
|
69
|
+
['UniqueID', user_id.to_s],
|
70
|
+
['PrimaryGroupID', group_id.to_s],
|
71
|
+
['NFSHomeDirectory', home_dir],
|
72
|
+
].each do |command_suffix|
|
73
|
+
command = command_prefix + command_suffix
|
74
|
+
unless Kernel.system(*command)
|
75
|
+
raise RuntimeError, "User creation failed at #{command.inspect}!"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
elsif RUBY_PLATFORM =~ /win/ # Windows
|
80
|
+
raise 'Windows is not supported; patches welcome!'
|
81
|
+
|
82
|
+
else # Linux
|
83
|
+
if group_id
|
84
|
+
command = ['useradd', '--gid', group_id.to_s,
|
85
|
+
'--no-create-home', '--no-user-group', user_name]
|
86
|
+
else
|
87
|
+
command = ['useradd', '--no-create-home', user_name]
|
88
|
+
end
|
89
|
+
unless Kernel.system(*command)
|
90
|
+
raise RuntimeError, "User creation failed at #{command.inspect}!"
|
91
|
+
end
|
92
|
+
|
93
|
+
home_dir = File.join '/home', user_name
|
94
|
+
user_id = Etc.getpwnam(user_name).uid
|
95
|
+
group_id = Etc.getpwnam(user_name).gid
|
96
|
+
end # RUBY_PLATFORM
|
97
|
+
|
98
|
+
FileUtils.mkdir_p home_dir
|
99
|
+
FileUtils.chown_R user_id, group_id, home_dir
|
100
|
+
FileUtils.chmod_R 0750, home_dir
|
101
|
+
|
102
|
+
user_id
|
103
|
+
end
|
104
|
+
|
105
|
+
# Removes a user that was previously created by create.
|
106
|
+
#
|
107
|
+
# @param [String] user_name the user's short (UNIX) name
|
108
|
+
def self.destroy(user_name)
|
109
|
+
user_pw = Etc.getpwnam(user_name)
|
110
|
+
home_dir = user_pw.dir
|
111
|
+
FileUtils.rm_rf home_dir
|
112
|
+
|
113
|
+
user_gid = user_pw.gid
|
114
|
+
group_name = Etc.getgrgid(user_gid).name
|
115
|
+
# If the group name matches the user name, the group is a temp.
|
116
|
+
destroy_group = user_name == group_name
|
117
|
+
|
118
|
+
if RUBY_PLATFORM =~ /darwin/ # OSX
|
119
|
+
command = ['dscl', '.', '-delete', "/Users/#{user_name}"]
|
120
|
+
unless Kernel.system(*command)
|
121
|
+
raise RuntimeError, "User removal failed at #{command.inspect}!"
|
122
|
+
end
|
123
|
+
|
124
|
+
if destroy_group
|
125
|
+
command = ['dscl', '.', '-delete', "/Groups/#{group_name}"]
|
126
|
+
unless Kernel.system(*command)
|
127
|
+
raise RuntimeError, "User removal failed at #{command.inspect}!"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
elsif RUBY_PLATFORM =~ /win/ # Windows
|
131
|
+
raise 'Windows is not supported; patches welcome!'
|
132
|
+
['userdel', git_user]
|
133
|
+
else # Linux
|
134
|
+
command = ['userdel', user_name]
|
135
|
+
unless Kernel.system(*command)
|
136
|
+
raise RuntimeError, "User removal failed at #{command.inspect}!"
|
137
|
+
end
|
138
|
+
if destroy_group
|
139
|
+
# Make sure that the group exists. userdel might remove it.
|
140
|
+
begin
|
141
|
+
Etc.getgrnam(group_name)
|
142
|
+
rescue ArgumentError
|
143
|
+
destroy_group = false
|
144
|
+
end
|
145
|
+
end
|
146
|
+
if destroy_group
|
147
|
+
command = ['groupdel', group_name]
|
148
|
+
unless Kernel.system(*command)
|
149
|
+
raise RuntimeError, "User removal failed at #{command.inspect}!"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end # RUBY_PLATFORM
|
153
|
+
end
|
154
|
+
end # module ExecSandbox::Users
|
155
|
+
|
156
|
+
end # namespace ExecSandbox
|