exec_sandbox 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
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