Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork33.3k
gh-97514: Authenticate the forkserver control socket.#99309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
d82afc872f3843c83193d14f6f4d8c5f7f4ca47b6f6f8e22f3bbbda70119b6aab9f93d7d41b166bb9db49c22c0607c01d4a53c01fdb29006File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -9,6 +9,7 @@ | ||
| import threading | ||
| import warnings | ||
| from . import AuthenticationError | ||
| from . import connection | ||
| from . import process | ||
| from .context import reduction | ||
| @@ -25,6 +26,7 @@ | ||
| MAXFDS_TO_SEND = 256 | ||
| SIGNED_STRUCT = struct.Struct('q') # large enough for pid_t | ||
| _AUTHKEY_LEN = 32 # <= PIPEBUF so it fits a single write to an empty pipe. | ||
| # | ||
| # Forkserver class | ||
| @@ -33,6 +35,7 @@ | ||
| class ForkServer(object): | ||
| def __init__(self): | ||
| self._forkserver_authkey = None | ||
| self._forkserver_address = None | ||
| self._forkserver_alive_fd = None | ||
| self._forkserver_pid = None | ||
| @@ -59,6 +62,7 @@ def _stop_unlocked(self): | ||
| if not util.is_abstract_socket_namespace(self._forkserver_address): | ||
| os.unlink(self._forkserver_address) | ||
| self._forkserver_address = None | ||
| self._forkserver_authkey = None | ||
| def set_forkserver_preload(self, modules_names): | ||
| '''Set list of module names to try to load in forkserver process.''' | ||
| @@ -83,6 +87,7 @@ def connect_to_new_process(self, fds): | ||
| process data. | ||
| ''' | ||
| self.ensure_running() | ||
| assert self._forkserver_authkey | ||
| if len(fds) + 4 >= MAXFDS_TO_SEND: | ||
| raise ValueError('too many fds') | ||
| with socket.socket(socket.AF_UNIX) as client: | ||
| @@ -93,6 +98,18 @@ def connect_to_new_process(self, fds): | ||
| resource_tracker.getfd()] | ||
| allfds += fds | ||
| try: | ||
| client.setblocking(True) | ||
| wrapped_client = connection.Connection(client.fileno()) | ||
| # The other side of this exchange happens in the child as | ||
| # implemented in main(). | ||
| try: | ||
| connection.answer_challenge( | ||
| wrapped_client, self._forkserver_authkey) | ||
| connection.deliver_challenge( | ||
| wrapped_client, self._forkserver_authkey) | ||
| finally: | ||
| wrapped_client._detach() | ||
| del wrapped_client | ||
| reduction.sendfds(client, allfds) | ||
| return parent_r, parent_w | ||
| except: | ||
| @@ -120,6 +137,7 @@ def ensure_running(self): | ||
| return | ||
| # dead, launch it again | ||
| os.close(self._forkserver_alive_fd) | ||
| self._forkserver_authkey = None | ||
| self._forkserver_address = None | ||
| self._forkserver_alive_fd = None | ||
| self._forkserver_pid = None | ||
| @@ -130,9 +148,9 @@ def ensure_running(self): | ||
| if self._preload_modules: | ||
| desired_keys = {'main_path', 'sys_path'} | ||
| data = spawn.get_preparation_data('ignore') | ||
| main_kws = {x: y for x, y in data.items() if x in desired_keys} | ||
| else: | ||
| main_kws = {} | ||
| with socket.socket(socket.AF_UNIX) as listener: | ||
| address = connection.arbitrary_address('AF_UNIX') | ||
| @@ -144,19 +162,31 @@ def ensure_running(self): | ||
| # all client processes own the write end of the "alive" pipe; | ||
| # when they all terminate the read end becomes ready. | ||
| alive_r, alive_w = os.pipe() | ||
| # A short lived pipe to initialize the forkserver authkey. | ||
| authkey_r, authkey_w = os.pipe() | ||
| try: | ||
| fds_to_pass = [listener.fileno(), alive_r, authkey_r] | ||
| main_kws['authkey_r'] = authkey_r | ||
| cmd %= (listener.fileno(), alive_r, self._preload_modules, | ||
| main_kws) | ||
gpshead marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| exe = spawn.get_executable() | ||
| args = [exe] + util._args_from_interpreter_flags() | ||
| args += ['-c', cmd] | ||
| pid = util.spawnv_passfds(exe, args, fds_to_pass) | ||
| except: | ||
| os.close(alive_w) | ||
| os.close(authkey_w) | ||
| raise | ||
| finally: | ||
| os.close(alive_r) | ||
| os.close(authkey_r) | ||
| # Authenticate our control socket to prevent access from | ||
| # processes we have not shared this key with. | ||
| try: | ||
| self._forkserver_authkey = os.urandom(_AUTHKEY_LEN) | ||
| os.write(authkey_w, self._forkserver_authkey) | ||
| finally: | ||
| os.close(authkey_w) | ||
| self._forkserver_address = address | ||
| self._forkserver_alive_fd = alive_w | ||
| self._forkserver_pid = pid | ||
| @@ -165,8 +195,18 @@ def ensure_running(self): | ||
| # | ||
| # | ||
| def main(listener_fd, alive_r, preload, main_path=None, sys_path=None, | ||
| *, authkey_r=None): | ||
gpshead marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| """Run forkserver.""" | ||
| if authkey_r is not None: | ||
| try: | ||
| authkey = os.read(authkey_r, _AUTHKEY_LEN) | ||
| assert len(authkey) == _AUTHKEY_LEN, f'{len(authkey)} < {_AUTHKEY_LEN}' | ||
| finally: | ||
| os.close(authkey_r) | ||
| else: | ||
| authkey = b'' | ||
| if preload: | ||
| if sys_path is not None: | ||
| sys.path[:] = sys_path | ||
| @@ -257,8 +297,24 @@ def sigchld_handler(*_unused): | ||
| if listener in rfds: | ||
| # Incoming fork request | ||
| with listener.accept()[0] as s: | ||
| try: | ||
| if authkey: | ||
| wrapped_s = connection.Connection(s.fileno()) | ||
| # The other side of this exchange happens in | ||
| # in connect_to_new_process(). | ||
| try: | ||
| connection.deliver_challenge( | ||
| wrapped_s, authkey) | ||
| connection.answer_challenge( | ||
| wrapped_s, authkey) | ||
gpshead marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| finally: | ||
| wrapped_s._detach() | ||
| del wrapped_s | ||
| # Receive fds from client | ||
| fds = reduction.recvfds(s, MAXFDS_TO_SEND + 1) | ||
| except (EOFError, BrokenPipeError, AuthenticationError): | ||
| s.close() | ||
| continue | ||
| if len(fds) > MAXFDS_TO_SEND: | ||
| raise RuntimeError( | ||
| "Too many ({0:n}) fds to send".format( | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -139,15 +139,12 @@ def detach(self): | ||
| __all__ += ['DupFd', 'sendfds', 'recvfds'] | ||
| import array | ||
gpshead marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| def sendfds(sock, fds): | ||
| '''Send an array of fds over an AF_UNIX socket.''' | ||
| fds = array.array('i', fds) | ||
| msg = bytes([len(fds) % 256]) | ||
| sock.sendmsg([msg], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fds)]) | ||
| if sock.recv(1) != b'A': | ||
| raise RuntimeError('did not receive acknowledgement of fd') | ||
| def recvfds(sock, size): | ||
| @@ -158,8 +155,11 @@ def recvfds(sock, size): | ||
| if not msg and not ancdata: | ||
| raise EOFError | ||
| try: | ||
| # We send/recv an Ack byte after the fds to work around an old | ||
| # macOS bug; it isn't clear if this is still required but it | ||
| # makes unit testing fd sending easier. | ||
| # See: https://github.com/python/cpython/issues/58874 | ||
| sock.send(b'A') # Acknowledge | ||
| if len(ancdata) != 1: | ||
| raise RuntimeError('received %d items of ancdata' % | ||
| len(ancdata)) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| Authentication was added to the :mod:`multiprocessing` forkserver start | ||
| method control socket so that only processes with the authentication key | ||
| generated by the process that spawned the forkserver can control it. This | ||
| is an enhancement over the other :gh:`97514` fixes so that access is no | ||
| longer limited only by filesystem permissions. | ||
| The file descriptor exchange of control pipes with the forked worker process | ||
| now requires an explicit acknowledgement byte to be sent over the socket after | ||
| the exchange on all forkserver supporting platforms. That makes testing the | ||
| above much easier. |
Uh oh!
There was an error while loading.Please reload this page.