Skip to content

Commit d82afc8

Browse files
committed
Authenticate the forkserver control socket.
This adds authentication. In the past only filesystem permissions protected this socket from code injection into the forkserver process by limiting access to the same UID, which didn't exist when Linux abstract namespace sockets were used (see issue) meaning that any process in the same system network namespace could inject code. This reuses the hmac based shared key auth already used on multiprocessing sockets used for other purposes. Doing this is useful so that filesystem permissions are not relied upon and trust isn't implied by default between all processes running as the same UID.
1 parent 87f5180 commit d82afc8

File tree

2 files changed

+56
-6
lines changed

2 files changed

+56
-6
lines changed

Lib/multiprocessing/connection.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,13 @@ def close(self):
178178
finally:
179179
self._handle = None
180180

181+
def _detach(self):
182+
"""Stop managing the underlying file descriptor or handle."""
183+
try:
184+
return self._handle
185+
finally:
186+
self._handle = None
187+
181188
def send_bytes(self, buf, offset=0, size=None):
182189
"""Send the bytes data from a bytes-like object"""
183190
self._check_closed()

Lib/multiprocessing/forkserver.py

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
MAXFDS_TO_SEND = 256
2626
SIGNED_STRUCT = struct.Struct('q') # large enough for pid_t
27+
_authkey_len = 32 # <= PIPEBUF so it fits a single write to an empty pipe.
2728

2829
#
2930
# Forkserver class
@@ -32,6 +33,7 @@
3233
class ForkServer(object):
3334

3435
def __init__(self):
36+
self._forkserver_authkey = None
3537
self._forkserver_address = None
3638
self._forkserver_alive_fd = None
3739
self._forkserver_pid = None
@@ -58,6 +60,7 @@ def _stop_unlocked(self):
5860
if not util.is_abstract_socket_namespace(self._forkserver_address):
5961
os.unlink(self._forkserver_address)
6062
self._forkserver_address = None
63+
self._forkserver_authkey = None
6164

6265
def set_forkserver_preload(self, modules_names):
6366
'''Set list of module names to try to load in forkserver process.'''
@@ -92,6 +95,16 @@ def connect_to_new_process(self, fds):
9295
resource_tracker.getfd()]
9396
allfds += fds
9497
try:
98+
if self._forkserver_authkey:
99+
client.setblocking(True)
100+
wrapped_client = connection.Connection(client.fileno())
101+
try:
102+
connection.answer_challenge(
103+
wrapped_client, self._forkserver_authkey)
104+
connection.deliver_challenge(
105+
wrapped_client, self._forkserver_authkey)
106+
finally:
107+
wrapped_client._detach()
95108
reduction.sendfds(client, allfds)
96109
return parent_r, parent_w
97110
except:
@@ -119,6 +132,7 @@ def ensure_running(self):
119132
return
120133
# dead, launch it again
121134
os.close(self._forkserver_alive_fd)
135+
self._forkserver_authkey = None
122136
self._forkserver_address = None
123137
self._forkserver_alive_fd = None
124138
self._forkserver_pid = None
@@ -129,9 +143,9 @@ def ensure_running(self):
129143
if self._preload_modules:
130144
desired_keys = {'main_path', 'sys_path'}
131145
data = spawn.get_preparation_data('ignore')
132-
data = {x: y for x, y in data.items() if x in desired_keys}
146+
main_kws = {x: y for x, y in data.items() if x in desired_keys}
133147
else:
134-
data = {}
148+
main_kws = {}
135149

136150
with socket.socket(socket.AF_UNIX) as listener:
137151
address = connection.arbitrary_address('AF_UNIX')
@@ -143,19 +157,31 @@ def ensure_running(self):
143157
# all client processes own the write end of the "alive" pipe;
144158
# when they all terminate the read end becomes ready.
145159
alive_r, alive_w = os.pipe()
160+
# A short lived pipe to initialize the forkserver authkey.
161+
authkey_r, authkey_w = os.pipe()
146162
try:
147-
fds_to_pass = [listener.fileno(), alive_r]
163+
fds_to_pass = [listener.fileno(), alive_r, authkey_r]
164+
main_kws['authkey_r'] = authkey_r
148165
cmd %= (listener.fileno(), alive_r, self._preload_modules,
149-
data)
166+
main_kws)
150167
exe = spawn.get_executable()
151168
args = [exe] + util._args_from_interpreter_flags()
152169
args += ['-c', cmd]
153170
pid = util.spawnv_passfds(exe, args, fds_to_pass)
154171
except:
155172
os.close(alive_w)
173+
os.close(authkey_w)
156174
raise
157175
finally:
158176
os.close(alive_r)
177+
os.close(authkey_r)
178+
# Prevent access from processes not in our process tree that
179+
# have the same shared key for this forkserver.
180+
try:
181+
self._forkserver_authkey = os.urandom(_authkey_len)
182+
os.write(authkey_w, self._forkserver_authkey)
183+
finally:
184+
os.close(authkey_w)
159185
self._forkserver_address = address
160186
self._forkserver_alive_fd = alive_w
161187
self._forkserver_pid = pid
@@ -164,8 +190,18 @@ def ensure_running(self):
164190
#
165191
#
166192

167-
def main(listener_fd, alive_r, preload, main_path=None, sys_path=None):
168-
'''Run forkserver.'''
193+
def main(listener_fd, alive_r, preload, main_path=None, sys_path=None,
194+
*, authkey_r=None):
195+
"""Run forkserver."""
196+
if authkey_r is not None:
197+
# If there is no authkey, the parent closes the pipe without writing
198+
# anything resulting in an empty authkey of b'' here.
199+
authkey = os.read(authkey_r, _authkey_len)
200+
assert len(authkey) == _authkey_len or not authkey
201+
os.close(authkey_r)
202+
else:
203+
authkey = b''
204+
169205
if preload:
170206
if '__main__' in preload and main_path is not None:
171207
process.current_process()._inheriting = True
@@ -254,6 +290,13 @@ def sigchld_handler(*_unused):
254290
if listener in rfds:
255291
# Incoming fork request
256292
with listener.accept()[0] as s:
293+
if authkey:
294+
wrapped_s = connection.Connection(s.fileno())
295+
try:
296+
connection.deliver_challenge(wrapped_s, authkey)
297+
connection.answer_challenge(wrapped_s, authkey)
298+
finally:
299+
wrapped_s._detach()
257300
# Receive fds from client
258301
fds = reduction.recvfds(s, MAXFDS_TO_SEND + 1)
259302
if len(fds) > MAXFDS_TO_SEND:

0 commit comments

Comments
 (0)