/
spawn.rb
155 lines (145 loc) · 5.17 KB
/
spawn.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# namespace
module ExecSandbox
# Manages sandboxed processes.
module Spawn
# Spawns a child process.
#
# @param [String, Array] command the command to be executed via exec
# @param [Hash] io see limit_io
# @param [Hash] principal the principal for the enw process
# @param [Hash] resources see limit_resources
# @return [Fixnum] the child's PID
def self.spawn(command, io = {}, principal = {}, resources = {})
fork do
limit_io io
limit_resources resources
set_principal principal
if command.respond_to? :to_str
Process.exec command
else
Process.exec *command
end
end
end
# Constraints the available file descriptors.
#
# @param [Hash] io associates file descriptors with IO objects or file paths;
# all file descriptors not covered by io will be closed
def self.limit_io(io)
# Sort the list of redirections by file descriptor number.
redirects = []
[:in, :out, :err].each_with_index do |sym, fd_num|
if target = io[sym]
redirects << [fd_num, redirects.length, target]
end
end
io.each do |k, v|
if k.kind_of? Integer
redirects << [k, redirects.length, v]
end
end
# Perform the redirections.
redirects.sort!
redirects.each do |fd_num, _, target|
if target.respond_to?(:fileno)
# IO stream.
if target.fileno != fd_num
LibC.close fd_num
LibC.dup2 target.fileno, fd_num
end
else
# Filename string.
LibC.close fd_num
open_fd = IO.sysopen(target, 'r+')
if open_fd != fd_num
LibC.dup2 open_fd, fd_num
LibC.close open_fd
end
end
end
# Close all file descriptors not in the redirection table.
redirected_fds = Set.new redirects.map(&:first)
max_fd = LibC.getdtablesize
0.upto(max_fd) do |fd|
LibC.close fd unless redirected_fds.include?(fd)
end
end
# Sets the process' principal for access control.
#
# @param [Hash] principal information about the process' principal
# @option principal [String] :dir the process' working directory
# @option principal [Fixnum] :uid the new user ID
# @option principal [Fixnum] :gid the new group ID
def self.set_principal(principal)
Dir.chdir principal[:dir] if principal[:dir]
if principal[:gid]
begin
Process::Sys.setresgid principal[:gid], principal[:gid], principal[:gid]
rescue NotImplementedError
Process.gid = principal[:gid]
Process.egid = principal[:gid]
end
end
if principal[:uid]
begin
Process.initgroups Etc.getpwuid(principal[:uid]).name,
principal[:gid] || Process.gid
rescue NotImplementedError
end
begin
Process::Sys.setresuid principal[:uid], principal[:uid], principal[:uid]
rescue NotImplementedError
Process.uid = principal[:uid]
Process.euid = principal[:uid]
end
end
end
# Constrains the resource usage of the current process.
#
# @param [Hash{Symbol => Number}] limits the constraints to be applied
# @option limits [Fixnum] :cpu maximum CPU time (for best results, give it an
# extra second, and measure actual resource usage after the process
# completes)
# @option limits [Fixnum] :processes number of processes that can be spawned
# by the user who owns this process (useful in conjunction with temporary
# users)
# @option limits [Fixnum] :file_size maximum size of a file created by the
# process; the process can still fill the disk by creating many files of
# this size
# @option limits [Fixnum] :open_files maximum number of open files; remember
# that any process uses 3 open files for STDIN, STDOUT, and STDERR
# @option limits [Fixnum] :data maximum data segment size (static data plus
# heap) and stack; allow slack for the libraries used by the process;
# mostly useful to prevent a process from freezing the machine by pushing
# everything into swap
def self.limit_resources(limits)
if limits[:cpu]
Process.setrlimit Process::RLIMIT_CPU, limits[:cpu], limits[:cpu]
end
if limits[:processes]
Process.setrlimit Process::RLIMIT_NPROC, limits[:processes],
limits[:processes]
end
if limits[:file_size]
Process.setrlimit Process::RLIMIT_FSIZE, limits[:file_size],
limits[:file_size]
end
if limits[:open_files]
Process.setrlimit Process::RLIMIT_NOFILE, limits[:open_files],
limits[:open_files]
end
if limits[:data]
Process.setrlimit Process::RLIMIT_DATA, limits[:data], limits[:data]
Process.setrlimit Process::RLIMIT_STACK, limits[:data], limits[:data]
end
end
# Maps raw I/O functions.
module LibC
extend FFI::Library
ffi_lib FFI::Library::LIBC
attach_function :close, [:int], :int
attach_function :getdtablesize, [], :int
attach_function :dup2, [:int, :int], :int
end # module ExecSandbox::Spawn::Libc
end # module ExecSandbox::Spawn
end # namespace ExecSandbox