diff --git a/newsfragments/860.bugfix.rst b/newsfragments/860.bugfix.rst
new file mode 100644
index 00000000..a0af6d9d
--- /dev/null
+++ b/newsfragments/860.bugfix.rst
@@ -0,0 +1,4 @@
+:exc:`trio.Cancelled` exceptions now always propagate until they reach
+the outermost unshielded cancelled scope, even if more cancellations
+occur or shielding is changed between when the :exc:`~trio.Cancelled`
+is delivered and when it is caught.
diff --git a/trio/_core/_exceptions.py b/trio/_core/_exceptions.py
index 937da232..6b4302e3 100644
--- a/trio/_core/_exceptions.py
+++ b/trio/_core/_exceptions.py
@@ -46,11 +46,9 @@ class Cancelled(BaseException):
 
     then this *won't* catch a :exc:`Cancelled` exception.
 
-    Attempting to raise :exc:`Cancelled` yourself will cause a
-    :exc:`RuntimeError`. It would not be associated with a cancel scope and thus
-    not be caught by Trio. Use
-    :meth:`cancel_scope.cancel() <trio.CancelScope.cancel>`
-    instead.
+    You cannot raise :exc:`Cancelled` yourself. Attempting to do so
+    will produce a :exc:`RuntimeError`. Use :meth:`cancel_scope.cancel()
+    <trio.CancelScope.cancel>` instead.
 
     .. note::
 
@@ -65,7 +63,6 @@ class Cancelled(BaseException):
        everywhere.
 
     """
-    _scope = None
     __marker = object()
 
     def __init__(self, _marker=None):
diff --git a/trio/_core/_run.py b/trio/_core/_run.py
index 98b44039..e00295a1 100644
--- a/trio/_core/_run.py
+++ b/trio/_core/_run.py
@@ -342,15 +342,6 @@ class CancelScope:
             for task in self._tasks:
                 task._attempt_delivery_of_any_pending_cancel()
 
-    def _cancel_no_notify(self):
-        # returns the affected tasks
-        if not self.cancel_called:
-            with self._might_change_effective_deadline():
-                self.cancel_called = True
-            return self._tasks
-        else:
-            return set()
-
     @enable_ki_protection
     def cancel(self):
         """Cancels this scope immediately.
@@ -358,7 +349,11 @@ class CancelScope:
         This method is idempotent, i.e., if the scope was already
         cancelled then this method silently does nothing.
         """
-        for task in self._cancel_no_notify():
+        if self.cancel_called:
+            return
+        with self._might_change_effective_deadline():
+            self.cancel_called = True
+        for task in self._tasks:
             task._attempt_delivery_of_any_pending_cancel()
 
     def _add_task(self, task):
@@ -379,24 +374,22 @@ class CancelScope:
     def _tasks_added_by_adoption(self, tasks):
         self._tasks.update(tasks)
 
-    def _make_exc(self):
-        exc = Cancelled._init()
-        exc._scope = self
-        return exc
-
     def _exc_filter(self, exc):
-        if isinstance(exc, Cancelled) and exc._scope is self:
+        if (
+            isinstance(exc, Cancelled) and self.cancel_called
+            and self._scope_task._pending_cancel_scope() is self
+        ):
             self.cancelled_caught = True
             return None
         return exc
 
     def _close(self, exc):
+        if exc is not None:
+            exc = MultiError.filter(self._exc_filter, exc)
         with self._might_change_effective_deadline():
             self._remove_task(self._scope_task)
         self._scope_task = None
-        if exc is not None:
-            return MultiError.filter(self._exc_filter, exc)
-        return None
+        return exc
 
 
 @deprecated("0.10.0", issue=607, instead="trio.CancelScope")
@@ -432,9 +425,8 @@ class _TaskStatus:
 
         # If the old nursery is cancelled, then quietly quit now; the child
         # will eventually exit on its own, and we don't want to risk moving
-        # the children into a different scope while they might have
-        # propagating Cancelled exceptions that assume they're under the old
-        # scope.
+        # children that might have propagating Cancelled exceptions into
+        # a place with no cancelled cancel scopes to catch them.
         if _pending_cancel_scope(self._old_nursery._cancel_stack) is not None:
             return
 
@@ -758,13 +750,11 @@ class Task:
     def _attempt_delivery_of_any_pending_cancel(self):
         if self._abort_func is None:
             return
-        pending_scope = self._pending_cancel_scope()
-        if pending_scope is None:
+        if self._pending_cancel_scope() is None:
             return
-        exc = pending_scope._make_exc()
 
         def raise_cancel():
-            raise exc
+            raise Cancelled._init()
 
         self._attempt_abort(raise_cancel)
 
@@ -1523,21 +1513,16 @@ def run_impl(runner, async_fn, args):
         if runner.instruments:
             runner.instrument("after_io_wait", timeout)
 
+        # Process cancellations due to deadline expiry
         now = runner.clock.current_time()
-        # We process all timeouts in a batch and then notify tasks at the end
-        # to ensure that if multiple timeouts occur at once, then it's the
-        # outermost one that gets delivered.
-        cancelled_tasks = set()
         while runner.deadlines:
             (deadline, _), cancel_scope = runner.deadlines.peekitem(0)
             if deadline <= now:
                 # This removes the given scope from runner.deadlines:
-                cancelled_tasks.update(cancel_scope._cancel_no_notify())
+                cancel_scope.cancel()
                 idle_primed = False
             else:
                 break
-        for task in cancelled_tasks:
-            task._attempt_delivery_of_any_pending_cancel()
 
         if not runner.runq and idle_primed:
             while runner.waiting_for_idle:

Delay deciding which cancel scope a Cancelled exception belongs to
Right now, whenever we create a `Cancelled` exception, we immediately tag it with the cancel scope that's going to catch it.

There's another option that's briefly mentioned in #606: we could wait until `CancelScope.__exit__` to decide whether a given `Cancelled` exception "belongs" to this scope. The check would be "on the current task's stack, am I the outermost unshielded scope that's in the cancelled state?"

This can behave differently from what we have now in cases where there are multiple cancelled `CancelScope`s on the stack.

I'm not entirely sure if this would actually help with #606, because of complications noted there (hello `KeyboardInterrupt`). But it would probably simplify the cancel scope code in general, in particular `_cancel_no_notify`, and the issues described in this comment: https://github.com/python-trio/trio/pull/835#issuecomment-455107324
diff --git a/trio/_core/tests/test_run.py b/trio/_core/tests/test_run.py
index c1d2aa0b..e03f6da0 100644
--- a/trio/_core/tests/test_run.py
+++ b/trio/_core/tests/test_run.py
@@ -637,21 +637,21 @@ async def test_cancel_scope_multierror_filtering():
                     nursery.start_soon(crasher)  # t4
                 # and then our __aexit__ also receives an outer Cancelled
             except _core.MultiError as multi_exc:
-                # This is outside the nursery scope but inside the outer
-                # scope, so the nursery should have absorbed t1 and t2's
-                # exceptions but t3 and t4 should remain, plus the Cancelled
-                # from 'outer'
-                assert len(multi_exc.exceptions) == 3
+                # Since the outer scope became cancelled before the
+                # nursery block exited, all cancellations inside the
+                # nursery block continue propagating to reach the
+                # outer scope.
+                assert len(multi_exc.exceptions) == 5
                 summary = {}
                 for exc in multi_exc.exceptions:
                     summary.setdefault(type(exc), 0)
                     summary[type(exc)] += 1
-                assert summary == {_core.Cancelled: 2, KeyError: 1}
+                assert summary == {_core.Cancelled: 4, KeyError: 1}
                 raise
     except AssertionError:  # pragma: no cover
         raise
     except BaseException as exc:
-        # This is ouside the outer scope, so the two outer Cancelled
+        # This is ouside the outer scope, so all the Cancelled
         # exceptions should have been absorbed, leaving just a regular
         # KeyError from crasher()
         assert type(exc) is KeyError
@@ -1400,7 +1400,7 @@ def test_TrioToken_run_sync_soon_starvation_resistance():
     assert len(record) == 2
     assert record[0] == "starting"
     assert record[1][0] == "run finished"
-    assert record[1][1] >= 20
+    assert record[1][1] >= 19
 
 
 def test_TrioToken_run_sync_soon_threaded_stress_test():
diff --git a/trio/tests/test_highlevel_open_tcp_stream.py b/trio/tests/test_highlevel_open_tcp_stream.py
index 9f7d5d88..35a5619e 100644
--- a/trio/tests/test_highlevel_open_tcp_stream.py
+++ b/trio/tests/test_highlevel_open_tcp_stream.py
@@ -397,9 +397,11 @@ async def test_multi_success(autojump_clock):
         happy_eyeballs_delay=1,
     )
     assert not scenario.sockets["1.1.1.1"].succeeded
-    assert scenario.sockets["2.2.2.2"].succeeded
-    assert scenario.sockets["3.3.3.3"].succeeded
-    assert scenario.sockets["4.4.4.4"].succeeded
+    assert (
+        scenario.sockets["2.2.2.2"].succeeded
+        or scenario.sockets["3.3.3.3"].succeeded
+        or scenario.sockets["4.4.4.4"].succeeded
+    )
     assert not scenario.sockets["5.5.5.5"].succeeded
     assert sock.ip in ["2.2.2.2", "3.3.3.3", "4.4.4.4"]
     assert trio.current_time() == (0.5 + 10)

pip install -e .
parse_log_pytest
pytest
pytest pytest-cov pytest-faulthandler pytest-xdist pytest-mock pytest-asyncio
apt-get update apt-get install -y gcc
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
async-generator==1.10
attrs @ file:///croot/attrs_1668696182826/work
certifi @ file:///croot/certifi_1671487769961/work/certifi
coverage==7.2.7
execnet==2.0.2
flit_core @ file:///opt/conda/conda-bld/flit-core_1644941570762/work/source/flit_core
idna==3.10
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1648562407465/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
outcome==1.3.0.post0
packaging @ file:///croot/packaging_1671697413597/work
pluggy @ file:///tmp/build/80754af9/pluggy_1648042572264/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pytest==7.1.2
pytest-asyncio==0.21.2
pytest-cov==4.1.0
pytest-faulthandler==2.0.1
pytest-mock==3.11.1
pytest-xdist==3.5.0
sniffio==1.3.1
sortedcontainers==2.4.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
-e git+https://github.com/python-trio/trio.git@319327d48dd2a6c143072f4627b82c42f1adfac9#egg=trio
typing_extensions @ file:///croot/typing_extensions_1669924550328/work
zipp @ file:///croot/zipp_1672387121353/work

name: trio
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=22.1.0=py37h06a4308_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - flit-core=3.6.0=pyhd3eb1b0_0
  - importlib-metadata=4.11.3=py37h06a4308_0
  - importlib_metadata=4.11.3=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=22.0=py37h06a4308_0
  - pip=22.3.1=py37h06a4308_0
  - pluggy=1.0.0=py37h06a4308_1
  - py=1.11.0=pyhd3eb1b0_0
  - pytest=7.1.2=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py37h06a4308_0
  - typing_extensions=4.4.0=py37h06a4308_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.11.0=py37h06a4308_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - async-generator==1.10
      - coverage==7.2.7
      - execnet==2.0.2
      - idna==3.10
      - outcome==1.3.0.post0
      - pytest-asyncio==0.21.2
      - pytest-cov==4.1.0
      - pytest-faulthandler==2.0.1
      - pytest-mock==3.11.1
      - pytest-xdist==3.5.0
      - sniffio==1.3.1
      - sortedcontainers==2.4.0
      - trio==0.10.0+dev
prefix: /opt/conda/envs/trio

trio/_core/tests/test_run.py::test_cancel_scope_multierror_filtering
trio/_core/tests/test_run.py::test_basic trio/_core/tests/test_run.py::test_initial_task_error trio/_core/tests/test_run.py::test_run_nesting trio/_core/tests/test_run.py::test_nursery_warn_use_async_with trio/_core/tests/test_run.py::test_nursery_main_block_error_basic trio/_core/tests/test_run.py::test_child_crash_basic trio/_core/tests/test_run.py::test_basic_interleave trio/_core/tests/test_run.py::test_task_crash_propagation trio/_core/tests/test_run.py::test_main_and_task_both_crash trio/_core/tests/test_run.py::test_two_child_crashes trio/_core/tests/test_run.py::test_child_crash_wakes_parent trio/_core/tests/test_run.py::test_reschedule trio/_core/tests/test_run.py::test_current_time trio/_core/tests/test_run.py::test_current_time_with_mock_clock trio/_core/tests/test_run.py::test_current_clock trio/_core/tests/test_run.py::test_current_task trio/_core/tests/test_run.py::test_root_task trio/_core/tests/test_run.py::test_out_of_context trio/_core/tests/test_run.py::test_current_statistics trio/_core/tests/test_run.py::test_instruments trio/_core/tests/test_run.py::test_instruments_interleave trio/_core/tests/test_run.py::test_null_instrument trio/_core/tests/test_run.py::test_instrument_before_after_run trio/_core/tests/test_run.py::test_instrument_task_spawn_exit trio/_core/tests/test_run.py::test_instruments_crash trio/_core/tests/test_run.py::test_cancel_scope_repr trio/_core/tests/test_run.py::test_cancel_points trio/_core/tests/test_run.py::test_cancel_edge_cases trio/_core/tests/test_run.py::test_precancelled_task trio/_core/tests/test_run.py::test_cancel_shielding trio/_core/tests/test_run.py::test_cancel_inheritance trio/_core/tests/test_run.py::test_cancel_shield_abort trio/_core/tests/test_run.py::test_basic_timeout trio/_core/tests/test_run.py::test_cancel_scope_deprecated trio/_core/tests/test_run.py::test_cancel_scope_nesting trio/_core/tests/test_run.py::test_cancel_unbound trio/_core/tests/test_run.py::test_timekeeping trio/_core/tests/test_run.py::test_failed_abort trio/_core/tests/test_run.py::test_broken_abort trio/_core/tests/test_run.py::test_error_in_run_loop trio/_core/tests/test_run.py::test_spawn_system_task trio/_core/tests/test_run.py::test_system_task_crash trio/_core/tests/test_run.py::test_system_task_crash_MultiError trio/_core/tests/test_run.py::test_system_task_crash_plus_Cancelled trio/_core/tests/test_run.py::test_system_task_crash_KeyboardInterrupt trio/_core/tests/test_run.py::test_yield_briefly_checks_for_timeout trio/_core/tests/test_run.py::test_exc_info trio/_core/tests/test_run.py::test_exc_info_after_yield_error trio/_core/tests/test_run.py::test_exception_chaining_after_yield_error trio/_core/tests/test_run.py::test_nursery_exception_chaining_doesnt_make_context_loops trio/_core/tests/test_run.py::test_TrioToken_identity trio/_core/tests/test_run.py::test_TrioToken_run_sync_soon_basic trio/_core/tests/test_run.py::test_TrioToken_run_sync_soon_too_late trio/_core/tests/test_run.py::test_TrioToken_run_sync_soon_idempotent trio/_core/tests/test_run.py::test_TrioToken_run_sync_soon_idempotent_requeue trio/_core/tests/test_run.py::test_TrioToken_run_sync_soon_after_main_crash trio/_core/tests/test_run.py::test_TrioToken_run_sync_soon_crashes trio/_core/tests/test_run.py::test_TrioToken_run_sync_soon_FIFO trio/_core/tests/test_run.py::test_TrioToken_run_sync_soon_starvation_resistance trio/_core/tests/test_run.py::test_TrioToken_run_sync_soon_threaded_stress_test trio/_core/tests/test_run.py::test_TrioToken_run_sync_soon_massive_queue trio/_core/tests/test_run.py::test_slow_abort_basic trio/_core/tests/test_run.py::test_slow_abort_edge_cases trio/_core/tests/test_run.py::test_task_tree_introspection trio/_core/tests/test_run.py::test_nursery_closure trio/_core/tests/test_run.py::test_spawn_name trio/_core/tests/test_run.py::test_current_effective_deadline trio/_core/tests/test_run.py::test_nice_error_on_bad_calls_to_run_or_spawn trio/_core/tests/test_run.py::test_calling_asyncio_function_gives_nice_error trio/_core/tests/test_run.py::test_trivial_yields trio/_core/tests/test_run.py::test_nursery_start trio/_core/tests/test_run.py::test_task_nursery_stack trio/_core/tests/test_run.py::test_nursery_start_with_cancelled_nursery trio/_core/tests/test_run.py::test_nursery_start_keeps_nursery_open trio/_core/tests/test_run.py::test_nursery_explicit_exception trio/_core/tests/test_run.py::test_nursery_stop_iteration trio/_core/tests/test_run.py::test_nursery_stop_async_iteration trio/_core/tests/test_run.py::test_traceback_frame_removal trio/_core/tests/test_run.py::test_contextvar_support trio/_core/tests/test_run.py::test_contextvar_multitask trio/_core/tests/test_run.py::test_system_task_contexts trio/_core/tests/test_run.py::test_Cancelled_init trio/_core/tests/test_run.py::test_sniffio_integration trio/_core/tests/test_run.py::test_Task_custom_sleep_data trio/_core/tests/test_run.py::test_permanently_detach_coroutine_object trio/_core/tests/test_run.py::test_detach_and_reattach_coroutine_object trio/_core/tests/test_run.py::test_detached_coroutine_cancellation trio/tests/test_highlevel_open_tcp_stream.py::test_close_all trio/tests/test_highlevel_open_tcp_stream.py::test_reorder_for_rfc_6555_section_5_4 trio/tests/test_highlevel_open_tcp_stream.py::test_format_host_port trio/tests/test_highlevel_open_tcp_stream.py::test_open_tcp_stream_real_socket_smoketest trio/tests/test_highlevel_open_tcp_stream.py::test_open_tcp_stream_input_validation trio/tests/test_highlevel_open_tcp_stream.py::test_one_host_quick_success trio/tests/test_highlevel_open_tcp_stream.py::test_one_host_slow_success trio/tests/test_highlevel_open_tcp_stream.py::test_one_host_quick_fail trio/tests/test_highlevel_open_tcp_stream.py::test_one_host_slow_fail trio/tests/test_highlevel_open_tcp_stream.py::test_one_host_failed_after_connect trio/tests/test_highlevel_open_tcp_stream.py::test_basic_fallthrough trio/tests/test_highlevel_open_tcp_stream.py::test_early_success trio/tests/test_highlevel_open_tcp_stream.py::test_custom_delay trio/tests/test_highlevel_open_tcp_stream.py::test_custom_errors_expedite trio/tests/test_highlevel_open_tcp_stream.py::test_all_fail trio/tests/test_highlevel_open_tcp_stream.py::test_multi_success trio/tests/test_highlevel_open_tcp_stream.py::test_does_reorder trio/tests/test_highlevel_open_tcp_stream.py::test_handles_no_ipv4 trio/tests/test_highlevel_open_tcp_stream.py::test_handles_no_ipv6 trio/tests/test_highlevel_open_tcp_stream.py::test_no_hosts trio/tests/test_highlevel_open_tcp_stream.py::test_cancel
trio/_core/_exceptions.py trio/_core/_run.py newsfragments/860.bugfix.rst
trio/_core/_exceptions.py trio/_core/_run.py newsfragments/860.bugfix.rst