Skip to content

Commit bcca47d

Browse files
committed
issue #533: update routing to account for DEL_ROUTE propagation race
1 parent 3d72cf8 commit bcca47d

File tree

3 files changed

+71
-17
lines changed

3 files changed

+71
-17
lines changed

docs/changelog.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,18 @@ Core Library
198198
`closed` flag, preventing historical bugs where a double close could destroy
199199
descriptors belonging to unrelated streams.
200200

201+
* `#533 <https://github.com/dw/mitogen/issues/533>`_: routing accounts for
202+
a race between a parent sending a message to a child via an intermediary,
203+
where the child had recently disconnected, and ``DEL_ROUTE`` propagating from
204+
the intermediary to the parent, informing it that the child no longer exists.
205+
This condition is detected at the intermediary and a dead message is returned
206+
to the parent.
207+
208+
Previously since the intermediary had already removed its route for the
209+
child, the *route messages upwards* rule would be triggered, causing the
210+
message (with a privileged ``src_id``/``auth_id``) to be sent upstream,
211+
resulting in a ``bad auth_id`` log message and a hang.
212+
201213
* `#586 <https://github.com/dw/mitogen/issues/586>`_: fix import of
202214
:mod:`__main__` on later versions of Python 3 when running from the
203215
interactive console.

mitogen/core.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3272,34 +3272,48 @@ def _async_route(self, msg, in_stream=None):
32723272
))
32733273
return
32743274

3275-
# Perform source verification.
3275+
parent_stream = self._stream_by_id.get(mitogen.parent_id)
3276+
src_stream = self._stream_by_id.get(msg.src_id, parent_stream)
3277+
3278+
# When the ingress stream is known, verify the message was received on
3279+
# the same as the stream we would expect to receive messages from the
3280+
# src_id and auth_id. This is like Reverse Path Filtering in IP, and
3281+
# ensures messages from a privileged context cannot be spoofed by a
3282+
# child.
32763283
if in_stream:
3277-
parent = self._stream_by_id.get(mitogen.parent_id)
3278-
expect = self._stream_by_id.get(msg.auth_id, parent)
3279-
if in_stream != expect:
3284+
auth_stream = self._stream_by_id.get(msg.auth_id, parent_stream)
3285+
if in_stream != auth_stream:
32803286
LOG.error('%r: bad auth_id: got %r via %r, not %r: %r',
3281-
self, msg.auth_id, in_stream, expect, msg)
3287+
self, msg.auth_id, in_stream, auth_stream, msg)
32823288
return
32833289

3284-
if msg.src_id != msg.auth_id:
3285-
expect = self._stream_by_id.get(msg.src_id, parent)
3286-
if in_stream != expect:
3287-
LOG.error('%r: bad src_id: got %r via %r, not %r: %r',
3288-
self, msg.src_id, in_stream, expect, msg)
3289-
return
3290+
if msg.src_id != msg.auth_id and in_stream != src_stream:
3291+
LOG.error('%r: bad src_id: got %r via %r, not %r: %r',
3292+
self, msg.src_id, in_stream, src_stream, msg)
3293+
return
32903294

3295+
# If the stream's MitogenProtocol has auth_id set, copy it to the
3296+
# message. This allows subtrees to become privileged by stamping a
3297+
# parent's context ID. It is used by mitogen.unix to mark client
3298+
# streams (like Ansible WorkerProcess) as having the same rights as
3299+
# the parent.
32913300
if in_stream.protocol.auth_id is not None:
32923301
msg.auth_id = in_stream.protocol.auth_id
32933302

3294-
# Maintain a set of IDs the source ever communicated with.
3303+
# Record the IDs the source ever communicated with.
32953304
in_stream.protocol.egress_ids.add(msg.dst_id)
32963305

32973306
if msg.dst_id == mitogen.context_id:
32983307
return self._invoke(msg, in_stream)
32993308

33003309
out_stream = self._stream_by_id.get(msg.dst_id)
3301-
if out_stream is None:
3302-
out_stream = self._stream_by_id.get(mitogen.parent_id)
3310+
if (not out_stream) and (parent_stream != src_stream or not in_stream):
3311+
# No downstream route exists. The message could be from a child or
3312+
# ourselves for a parent, in which case we must forward it
3313+
# upstream, or it could be from a parent for a dead child, in which
3314+
# case its src_id/auth_id would fail verification if returned to
3315+
# the parent, so in that case reply with a dead message instead.
3316+
out_stream = parent_stream
33033317

33043318
if out_stream is None:
33053319
self._maybe_send_dead(True, msg, self.no_route_msg,
@@ -3310,9 +3324,9 @@ def _async_route(self, msg, in_stream=None):
33103324
(in_stream.protocol.is_privileged or
33113325
out_stream.protocol.is_privileged):
33123326
self._maybe_send_dead(True, msg, self.unidirectional_msg,
3313-
in_stream.protocol.remote_id,
3314-
out_stream.protocol.remote_id,
3315-
mitogen.context_id)
3327+
in_stream.protocol.remote_id,
3328+
out_stream.protocol.remote_id,
3329+
mitogen.context_id)
33163330
return
33173331

33183332
out_stream.protocol._send(msg)

tests/router_test.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,34 @@ def test_bad_auth_id(self):
7676
expect = 'bad auth_id: got %r via' % (self.child2_msg.auth_id,)
7777
self.assertTrue(expect in log.stop())
7878

79+
def test_parent_unaware_of_disconnect(self):
80+
# Parent -> Child A -> Child B. B disconnects concurrent to Parent
81+
# sending message. Parent does not yet know B has disconnected, A
82+
# receives message from Parent with Parent's auth_id, for a stream that
83+
# no longer exists.
84+
c1 = self.router.local()
85+
strm = self.router.stream_by_id(c1.context_id)
86+
recv = mitogen.core.Receiver(self.router)
87+
88+
self.broker.defer(lambda:
89+
strm.protocol._send(
90+
mitogen.core.Message(
91+
dst_id=1234, # nonexistent child
92+
handle=1234,
93+
src_id=mitogen.context_id,
94+
reply_to=recv.handle,
95+
)
96+
)
97+
)
98+
99+
e = self.assertRaises(mitogen.core.ChannelError,
100+
lambda: recv.get().unpickle()
101+
)
102+
self.assertEquals(e.args[0], self.router.no_route_msg % (
103+
1234,
104+
c1.context_id,
105+
))
106+
79107
def test_bad_src_id(self):
80108
# Deliver a message locally from child2 with the correct auth_id, but
81109
# the wrong src_id.

0 commit comments

Comments
 (0)