git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit3a57bab91b7de19953474bc1e5b41b01339e1f19
parentb07ff29a5a
authorLucas Galante <[email protected]>
date2026-07-12 15:06
docs(rfc): the 6bc borrowed-pointer retype design — WidgetId is the handle

Design pass for the machinery retype: every stored widget reference
becomes a WidgetId resolved through the generational WidgetTree at
use; raw pointers survive only as the registry payload. Slice list
recorded (focus, popovers/context-menu, container children,
propagate_event roots, window_runner).

 docs/rfc-core-rebuild.md | 43 ++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 38 insertions(+), 5 deletions(-)

diff --git a/docs/rfc-core-rebuild.md b/docs/rfc-core-rebuild.md
index 5903d62..6f15789 100644
--- a/docs/rfc-core-rebuild.md
+++ b/docs/rfc-core-rebuild.md
@@ -1748,11 +1748,44 @@ Constraint respected: **each crate still builds standalone** — the new core is
     `JsonWidget`'s `Box<dyn Element>`; the label walk dropped its
     `as_ptr` round-trip unsafe for a plain reborrow. **No owned
     type-erased widget storage remains anywhere in the workspace.**
-  - Remaining retype order: (1) the containers' borrowed child ptr Vecs
-    (`Vec<*mut dyn Element>` — pointers into app-owned concrete
-    storage; likely retyped together with (2) the cce-ui machinery core
-    (context.rs registry/focus/propagate, window_runner render
-    plumbing), and `Element` + `Adapted` die last.
+  - **The borrowed-pointer retype design (6bc, decided 2026-07-12).**
+    The replacement handle is **`WidgetId`, resolved through the
+    generational `WidgetTree` at every use**. Rationale: apps own
+    widgets concretely (the 6bb rosters) and re-register pointers
+    idempotently per frame from boxed storage, so the registry is the
+    one place a raw pointer is refreshed before use; every *other*
+    stored `*mut dyn Element` bypasses that guard and is exactly where
+    the real UAFs happened (display-manager 6ao — dangling registry
+    from a by-value `new()`; settings 6w — `FOCUSED_WIDGET` surviving a
+    rebuild via same-size alloc reuse). Under the retype, raw pointers
+    remain only (a) as the `WidgetTree` registry payload and (b) as
+    transient same-frame values inside resolution helpers; every stored
+    reference becomes a `WidgetId`, and a stale id resolves to `None`
+    and is skipped — the UAF class becomes unrepresentable outside the
+    registry itself. Public signatures taking `&dyn Element` stay
+    (deriving the id from `base()` internally) so most call sites
+    survive verbatim; direct field readers convert compiler-driven.
+    Slices, each independently shippable and A/B-verifiable:
+    1. **Focus** — both stores (`UiContext.focused_widget` AND the
+       `core.rs` thread-local `FOCUSED_WIDGET`) → `Option<WidgetId>`,
+       kept as two stores with their existing reader sets (merging
+       them changes observable focus behavior — not this phase's job).
+       Thread-local fns that must dispatch `unfocus`/`FocusOut` gain a
+       ctx/tree param (every dispatching call site has one in reach).
+       `set_focused` self-registers its target if unregistered, so
+       focus on a not-yet-linked widget keeps working.
+    2. **Popovers + context-menu target** — `active_popovers:
+       Vec<WidgetId>`, `ContextMenuState.target: Option<WidgetId>`;
+       the address-keyed coverage walk (`is_coordinate_covered`,
+       `EventCtx::widget_addr`) becomes id-keyed.
+    3. **Container child storage** (`ScrollBox.children`,
+       `ParametersBg.children`, Paginator) + the
+       `Element::children`/`parent`/`add_child`/`set_parent` surface →
+       ids; the propagate/paint walks resolve per step.
+    4. **`propagate_event(event, root: WidgetId)`** + the app dispatch
+       loops off `as_ptr_mut` (the big app sweep).
+    5. window_runner render plumbing + remaining `as_ptr` sites; then
+       the `Element` + `Adapted` endgame (own design pass).
 
 Order rationale: each phase is independently valuable and reversible, and no phase requires the
 next to compile. Phase 0 can land immediately regardless of the rest.