9
9
from Queue import Queue , Empty
10
10
from threading import Thread
11
11
12
+ try :
13
+ from server_info import server_info
14
+ _SECURE_DEFAULT = True
15
+ except ImportError :
16
+ _SECURE_DEFAULT = False
17
+
18
+ class JailError (StandardError ):
19
+ pass
20
+
21
+ class _Jail (object ):
22
+ def __init__ (self ):
23
+ self .locked = False
24
+ jail_base = "/srv/chroot"
25
+ all_jails = os .listdir (jail_base )
26
+ all_jails = [j for j in all_jails if j .startswith ("jailuser" )]
27
+ for jail in all_jails :
28
+ lock_dir = os .path .join (jail_base , jail , "locked" )
29
+ try :
30
+ os .mkdir (lock_dir )
31
+ except OSError :
32
+ # if the directory could not be created, that should mean the
33
+ # jail is already locked and in use
34
+ continue
35
+ with open (os .path .join (lock_dir , "lock.pid" ), "w" ) as pid_file :
36
+ pid_file .write (str (os .getpid ()))
37
+ self .locked = True
38
+ self .name = jail
39
+ break
40
+ else :
41
+ raise JailError ("Could not find an unlocked jail" )
42
+ self .jchown = os .path .join (server_info ["root_path" ],
43
+ server_info ["repo_path" ], "worker/jail_own" )
44
+ self .base_dir = os .path .join (jail_base , jail )
45
+ self .number = int (jail [len ("jailuser" ):])
46
+ self .chroot_cmd = "sudo -u {0} schroot -u {0} -c {0} -d {1} " .format (
47
+ self .name , "/home/jailuser" )
48
+
49
+ def __del__ (self ):
50
+ if self .locked :
51
+ raise JailError ("Jail object for %s freed without being released"
52
+ % (self .name ))
53
+
54
+ def release (self ):
55
+ if not self .locked :
56
+ raise JailError ("Attempt to release jail that is already unlocked" )
57
+ lock_dir = os .path .join (self .base_dir , "locked" )
58
+ pid_filename = os .path .join (lock_dir , "lock.pid" )
59
+ with open (pid_filename , 'r' ) as pid_file :
60
+ lock_pid = int (pid_file .read ())
61
+ if lock_pid != os .getpid ():
62
+ # if we ever get here something has gone seriously wrong
63
+ # most likely the jail locking mechanism has failed
64
+ raise JailError ("Jail released by different pid, name %s, lock_pid %d, release_pid %d"
65
+ % (self .name , lock_pid , os .getpid ()))
66
+ os .unlink (pid_filename )
67
+ os .rmdir (lock_dir )
68
+ self .locked = False
69
+
70
+ def prepare_with (self , command_dir ):
71
+ if os .system ("%s c %d" % (self .jchown , self .number )) != 0 :
72
+ raise JailError ("Error returned from jail_own c %d in prepare"
73
+ % (self .number ,))
74
+ scratch_dir = os .path .join (self .base_dir , "scratch" )
75
+ if os .system ("rm -rf %s" % (scratch_dir ,)) != 0 :
76
+ raise JailError ("Could not remove old scratch area from jail %d"
77
+ % (self .number ,))
78
+ home_dir = os .path .join (scratch_dir , "home/jailuser" )
79
+ os .makedirs (home_dir )
80
+ if os .system ("cp -r %s %s" % (command_dir , home_dir )) != 0 :
81
+ raise JailError ("Error copying working directory '%s' to jail %d"
82
+ % (command_dir , self .number ))
83
+ if os .system ("%s j %d" % (self .jchown , self .number )) != 0 :
84
+ raise JailError ("Error returned from jail_own j %d in prepare"
85
+ % (self .number ,))
86
+
87
+ def signal (self , signal ):
88
+ if not self .locked :
89
+ raise JailError ("Attempt to send %s to unlocked jail" % (signal ,))
90
+ if os .system ("sudo -u {0} killall -{1} -u {0}" .format (
91
+ self .name , signal )) != 0 :
92
+ raise JailError ("Error returned from jail kill for %s"
93
+ % (self .name ,))
94
+
95
+ def kill (self ):
96
+ self .signal ("KILL" )
97
+
98
+ def pause (self ):
99
+ self .signal ("STOP" )
100
+
101
+ def resume (self ):
102
+ self .signal ("CONT" )
103
+
104
+
12
105
def _monitor_input_channel (sandbox ):
13
106
while sandbox .is_alive :
14
107
try :
@@ -31,7 +124,8 @@ class Sandbox:
31
124
32
125
"""
33
126
34
- def __init__ (self , working_directory , shell_command , stderr = None ):
127
+ def __init__ (self , working_directory , shell_command , stderr = None ,
128
+ secure = _SECURE_DEFAULT ):
35
129
"""Initialize a new sandbox and invoke the given shell command inside.
36
130
37
131
working_directory: the directory in which the shell command should
@@ -40,15 +134,26 @@ def __init__(self, working_directory, shell_command, stderr=None):
40
134
shell command is executed.
41
135
shell_command: the shell command to launch inside the sandbox.
42
136
stderr: where the bot's stderr output should be written out to
137
+ defaults to keeping the current stderr for the child process
138
+ secure: really use a jail or just run the command directly
139
+ defaults to True when a server_info module is found, False
140
+ otherwise
43
141
44
142
"""
45
- shell_command = shell_command .replace ('\\ ' ,'/' )
46
143
self .is_alive = False
47
144
self .command_process = None
48
145
self .stdout_queue = Queue ()
49
- self .stderr_queue = Queue ()
50
146
51
- self .command_process = subprocess .Popen (shlex .split (shell_command ),
147
+ if secure :
148
+ self .jail = _Jail ()
149
+ self .jail .prepare_with (working_directory )
150
+ shell_command = self .jail .chroot_cmd + shell_command
151
+ working_directory = None
152
+ else :
153
+ self .jail = None
154
+
155
+ shell_command = shlex .split (shell_command .replace ('\\ ' ,'/' ))
156
+ self .command_process = subprocess .Popen (shell_command ,
52
157
stdin = subprocess .PIPE ,
53
158
stdout = subprocess .PIPE ,
54
159
stderr = stderr ,
@@ -57,41 +162,62 @@ def __init__(self, working_directory, shell_command, stderr=None):
57
162
stdout_monitor = Thread (target = _monitor_input_channel , args = (self ,))
58
163
stdout_monitor .start ()
59
164
165
+ def release (self ):
166
+ """Release the sandbox for further use
167
+
168
+ If running in a jail unlocks and releases the jail for reuse by others.
169
+ Must be called exactly once after Sandbox.kill has been called.
170
+
171
+ """
172
+ if self .is_alive :
173
+ raise JailError ("Jail released while still alive" )
174
+ if self .jail :
175
+ self .jail .release ()
176
+
60
177
def kill (self ):
61
- """ Shuts down the sandbox.
178
+ """Shuts down the sandbox.
62
179
63
180
Shuts down the sandbox, cleaning up any spawned processes, threads, and
64
181
other resources. The shell command running inside the sandbox may be
65
182
suddenly terminated.
66
183
67
184
"""
68
185
if self .is_alive :
69
- try :
70
- self .command_process .kill ()
71
- self .command_process .wait ()
72
- except OSError :
73
- pass
186
+ if self .jail :
187
+ self .jail .kill ()
188
+ else :
189
+ try :
190
+ self .command_process .kill ()
191
+ self .command_process .wait ()
192
+ except OSError :
193
+ pass
74
194
self .is_alive = False
75
195
76
196
def pause (self ):
77
197
"""Pause the process by sending a SIGSTOP to the child
78
198
79
199
This method is a no-op on Windows
80
200
"""
81
- try :
82
- self .command_process .send_signal (signal .SIGSTOP )
83
- except (ValueError , AttributeError ):
84
- pass
201
+ if self .jail :
202
+ self .jail .pause ()
203
+ else :
204
+ try :
205
+ self .command_process .send_signal (signal .SIGSTOP )
206
+ except (ValueError , AttributeError ):
207
+ pass
85
208
86
209
def resume (self ):
87
210
"""Resume the process by sending a SIGCONT to the child
88
211
89
212
This method is a no-op on Windows
90
213
"""
91
- try :
92
- self .command_process .send_signal (signal .SIGCONT )
93
- except (ValueError , AttributeError ):
94
- pass
214
+ if self .jail :
215
+ self .jail .resume ()
216
+ else :
217
+ try :
218
+ self .command_process .send_signal (signal .SIGCONT )
219
+ except (ValueError , AttributeError ):
220
+ pass
95
221
96
222
def write (self , str ):
97
223
"""Write str to stdin of the process being run"""
@@ -146,12 +272,18 @@ def main():
146
272
parser .add_option ("-r" , "--receive-delay" , action = "store" ,
147
273
dest = "resp_delay" , type = "float" , default = 0.01 ,
148
274
help = "Time in seconds to sleep before checking for a response line" )
275
+ parser .add_option ("-j" , "--jail" , action = "store_true" , dest = "secure" ,
276
+ default = _SECURE_DEFAULT ,
277
+ help = "Run in a secure jail" )
278
+ parser .add_option ("-o" , "--open" , action = "store_false" , dest = "secure" ,
279
+ help = "Run without using a secure jail" )
149
280
options , args = parser .parse_args ()
150
281
if len (args ) == 0 :
151
282
parser .error ("Must include a command to run.\
152
283
\n Run with --help for more information." )
153
284
154
- sandbox = Sandbox (options .working_dir , " " .join (args ))
285
+ sandbox = Sandbox (options .working_dir , " " .join (args ),
286
+ secure = options .secure )
155
287
for line in options .send_lines :
156
288
if not sandbox .write_line (line ):
157
289
print >> sys .stderr , "Could not send line '%s'" % (line ,)
0 commit comments